From 669da0bfa65d67ab54e342b7df1e414029badd92 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Wed, 20 May 2026 23:52:00 -0400 Subject: [PATCH 01/10] chore: add claude setup, and update some ai files --- .claude/settings.json | 16 + .claude/skills/plan-writing.md | 1 + .../instructions/jsdoc-tsdoc.instructions.md | 181 ++++++++++++ .github/instructions/jsdoc.instructions.md | 136 --------- .../instructions/performance.instructions.md | 67 ----- .github/instructions/security.instructions.md | 273 +++++++----------- .gitignore | 4 + CLAUDE.md | 34 +++ 8 files changed, 336 insertions(+), 376 deletions(-) create mode 100644 .claude/settings.json create mode 100644 .claude/skills/plan-writing.md create mode 100644 .github/instructions/jsdoc-tsdoc.instructions.md delete mode 100644 .github/instructions/jsdoc.instructions.md delete mode 100644 .github/instructions/performance.instructions.md create mode 100644 CLAUDE.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..eb996bf --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "enabledPlugins": { + "typescript-lsp@claude-plugins-official": true, + "superpowers@claude-plugins-official": true, + "code-review@claude-plugins-official": true, + "code-simplifier@claude-plugins-official": true, + "skill-creator@claude-plugins-official": true, + "github@claude-plugins-official": true, + "claude-md-management@claude-plugins-official": true, + "context7@claude-plugins-official": true, + "ralph-loop@claude-plugins-official": true, + "security-guidance@claude-plugins-official": true, + "claude-code-setup@claude-plugins-official": true, + "pr-review-toolkit@claude-plugins-official": true + } +} diff --git a/.claude/skills/plan-writing.md b/.claude/skills/plan-writing.md new file mode 100644 index 0000000..01fe6e8 --- /dev/null +++ b/.claude/skills/plan-writing.md @@ -0,0 +1 @@ +@.ai/skills/plan-writing.md diff --git a/.github/instructions/jsdoc-tsdoc.instructions.md b/.github/instructions/jsdoc-tsdoc.instructions.md new file mode 100644 index 0000000..f2b1643 --- /dev/null +++ b/.github/instructions/jsdoc-tsdoc.instructions.md @@ -0,0 +1,181 @@ +--- +applyTo: '**/*.js,**/*.mjs,**/*.ts,**/*.tsx' +--- + +# JSDoc & TSDoc Documentation Standards + +JavaScript files (`.js`, `.mjs`) use **JSDoc** with explicit `{Type}` +annotations because JavaScript has no type system. TypeScript files (`.ts`, +`.tsx`) use **TSDoc** — types already live in the signatures, so type +annotations are omitted from doc tags. + +## Core Principles (Both) + +1. **Every exported function must have a doc comment** — No exceptions. +2. **Include function description** — Clear explanation of what it does. +3. **Add `@example` for functions with I/O** — Show actual usage patterns. +4. **Add `@throws` for each error type** — Document every throw statement with + its specific error condition. + +--- + +## JSDoc — JavaScript Files (`.js`, `.mjs`) + +### Type Definition Rules + +**Use `import()` for external types:** + +```javascript +/** + * @typedef {import('node:fs').Stats} FileStats + * @typedef {import('./types.js').UserRecord} UserRecord + */ +``` + +**Define custom types as typedefs:** + +```javascript +/** + * @typedef {'low'|'medium'|'high'} Priority + * @typedef {Object} QueryFilters + * @property {string} [ownerId] - Owner identifier + * @property {number} [page=1] - Page number + */ +``` + +**Array types: use `Type[]`, not `Array`:** + +```javascript +@param {string[]} ids - User IDs +@returns {[number, number][]} Array of [min, max] pairs +``` + +### Complete Function Documentation + +```javascript +/** + * Retrieves records with optional pagination and filtering. + * + * @param {import('./repository.js').RecordRepository} repository - Data repository + * @param {QueryFilters} filters - Filter criteria + * @returns {Promise} Query results with metadata + * @throws {Error} When data retrieval fails + * + * @example + * const result = await getRecords(repository, { page: 1, limit: 50 }) + */ +async function getRecords(repository, filters) {} +``` + +### Common Patterns + +**Optional parameters with defaults:** + +```javascript +@param {number} [page=1] - Page number +@param {boolean} [includeCounts=false] - Include counts +``` + +**Destructured object parameters:** + +```javascript +/** + * @param {Object} options + * @param {string} options.name - User name + * @param {boolean} [options.active] - Whether the user is active + */ +function createUser({ name, active }) {} +``` + +**Multiple accepted types:** + +```javascript +@param {string|number} id - User ID +@returns {User|null} User object or null if not found +``` + +### Checklist + +- [ ] Every exported function has JSDoc +- [ ] All params documented with `{Type}` and description +- [ ] Return type documented with `{Type}` +- [ ] Optional params marked with `[param]` or `[param=default]` +- [ ] Array element types specified: `{string[]}` +- [ ] Async functions return `{Promise}` +- [ ] `@throws` present for every throw statement +- [ ] `@example` present for functions with parameters or return values + +--- + +## TSDoc — TypeScript Files (`.ts`, `.tsx`) + +Types live in the TypeScript signature. Do **not** include `{Type}` in `@param` +or `@returns` — TypeScript already provides that information. + +### Complete Function Documentation + +```typescript +/** + * Retrieves records with optional pagination and filtering. + * Adds computed metadata needed by downstream consumers. + * + * @param repository - Data repository instance. + * @param filters - Filter criteria for the query. + * @returns Query results with metadata attached. + * @throws {DataRetrievalError} When the data source is unavailable. + * + * @example + * const result = await getRecords(repository, { page: 1, limit: 50 }) + */ +async function getRecords( + repository: RecordRepository, + filters: QueryFilters, +): Promise {} +``` + +### Generic Type Parameters + +Document generic type parameters with `@typeParam`: + +```typescript +/** + * Wraps a value in an optional container. + * + * @typeParam T - The type of the wrapped value. + * @param value - Value to wrap. + * @returns The value wrapped in an optional container. + */ +function wrap(value: T): Optional {} +``` + +### Common Patterns + +**Optional parameters:** + +```typescript +/** + * @param data - Data to sanitize. + * @param options - Sanitization options. Uses defaults when omitted. + */ +function sanitize(data: unknown, options?: SanitizeOptions) {} +``` + +**Describe behavior, not type, in `@returns`:** + +```typescript +// Bad: just restates the return type +@returns {string} string + +// Good: describes what the value represents +@returns The sanitized string with all matched fields masked. +``` + +### Checklist + +- [ ] Every exported function has a TSDoc comment +- [ ] No `{Type}` annotations on `@param` or `@returns` +- [ ] All parameters documented with name and description +- [ ] `@returns` describes the value, not the type +- [ ] `@throws` present for every throw statement +- [ ] `@typeParam` used for generic type parameters +- [ ] `@example` present for functions with parameters or return values diff --git a/.github/instructions/jsdoc.instructions.md b/.github/instructions/jsdoc.instructions.md deleted file mode 100644 index ce9945c..0000000 --- a/.github/instructions/jsdoc.instructions.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -applyTo: '**/*.js,**/*.jsx,**/*.ts,**/*.tsx' ---- - -# JSDoc Documentation Standards - -## Core Principles - -1. **Every function must have JSDoc** — No exceptions. -2. **No inline type definitions** — Use typedefs or imported types. -3. **Import types from packages** — Use `import('package').Type` syntax -4. **Include function description** — Clear explanation of what it does -5. **Add @example for functions with I/O** — Show actual usage patterns -6. **Add @throws for each error type** — Document every throw statement with - specific error condition - -## Type Definition Rules - -**Use `import()` for external types:** - -```javascript -/** - * @typedef {import('node:fs').Stats} FileStats - * @typedef {import('node:http').IncomingHttpHeaders} IncomingHttpHeaders - * @typedef {import('./types.js').UserRecord} UserRecord - */ -``` - -**Define custom types as typedefs:** - -```javascript -/** - * @typedef {'low'|'medium'|'high'} Priority - * @typedef {Object} QueryFilters - * @property {string} [ownerId] - Owner identifier - * @property {string} [status] - Optional status filter - * @property {number} [page=1] - Page number - * @property {number} [limit=100] - Items per page - */ -``` - -**Use `[...]` for arrays, not `Array<...>`:** - -```javascript -@param {[number, number][]} ranges - Array of [min, max] pairs -@returns {string[]} Array of user IDs -``` - -## Complete Function Documentation - -Every function should include: - -1. **Description** — What the function does -2. **@param** — All parameters with imported/typedef types -3. **@returns** — Return type (use `Promise` for async) -4. **@throws** — Document error conditions -5. **@example** — Usage example for functions with parameters or return values - -```javascript -/** - * Retrieves records with optional pagination and filtering. - * Adds computed metadata needed by downstream consumers. - * - * @param {import('./repository.js').RecordRepository} repository - Data repository - * @param {QueryFilters} filters - Filter criteria - * @param {import('./logger.js').Logger} logger - Logger instance - * @returns {Promise} Query results with metadata - * @throws {Error} When data retrieval fails - * @throws {Error} When response shaping fails - * - * @example - * // Get records with pagination - * const result = await getRecords(repository, { - * ownerId: 'user-123', - * page: 1, - * limit: 50 - * }, logger) - * - * @example - * // Get records within a date range - * const result = await getRecords(repository, { - * ownerId: 'user-456', - * start_date: '2025-01-01T00:00:00Z', - * end_date: '2025-01-31T23:59:59Z' - * }, logger) - */ -async function getRecords(repository, filters, logger) { - // Implementation -} -``` - -## Common Patterns - -**Optional parameters with defaults:** - -```javascript -@param {number} [page=1] - Page number -@param {boolean} [include_counts=false] - Include counts -``` - -**Multiple type options:** - -```javascript -@param {string|number} id - User ID -@returns {User|null} User object or null if not found -``` - -**Destructured parameters:** - -```javascript -/** - * @param {Object} options - * @param {string} options.ownerId - Owner identifier - * @param {boolean} [options.includeCounts] - Include counts - */ -function process({ ownerId, includeCounts }) {} -``` - -**Callback/function types:** - -```javascript -@param {(error: Error|null, result: any) => void} callback -``` - -## Checklist - -When documenting types, verify: - -- [ ] Types match actual data passed to function -- [ ] Return types match actual returned data -- [ ] Optional params marked with `[param]` or `[param=default]` -- [ ] Nullable types marked with `|null` or `|undefined` -- [ ] Union types used for multiple accepted types -- [ ] No inline `Object` — use typedef or imported type -- [ ] Array element types specified: `[ElementType]` -- [ ] Async functions return `Promise` diff --git a/.github/instructions/performance.instructions.md b/.github/instructions/performance.instructions.md deleted file mode 100644 index 1ca2b06..0000000 --- a/.github/instructions/performance.instructions.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -applyTo: '**' ---- - -# Performance Guidelines - -## Core Principles - -1. **Readability first** — Optimize only when needed -2. **Profile before optimizing** — Measure, don't guess -3. **Document performance-critical code** — Explain why optimizations exist -4. **Split large files** — Break into focused modules when exceeding 300 lines - -## Memory Management - -**Always clean up resources:** - -```javascript -// Good: Cleanup event listeners -function startWatcher(target) { - const onChange = () => { - /* ... */ - }; - target.addEventListener('change', onChange); - return () => target.removeEventListener('change', onChange); -} - -// Good: Cleanup timers -const timer = setInterval(runWork, 1000); -clearInterval(timer); -``` - -## Compute Optimization - -**Cache expensive calculations when inputs are stable:** - -```javascript -const cache = new Map(); - -function computeDigest(input) { - if (cache.has(input)) return cache.get(input); - const result = expensiveOperation(input); - cache.set(input, result); - return result; -} -``` - -## Code Splitting - -**Lazy load heavy modules:** - -```javascript -async function loadAnalyzer() { - const { analyze } = await import('./analyzer.js'); - return analyze; -} -``` - -## Checklist - -- [ ] Event listeners and timers cleaned up -- [ ] Large objects released when no longer needed -- [ ] Expensive calculations cached where beneficial -- [ ] Heavy modules lazy loaded where practical -- [ ] Files split when exceeding 300 lines -- [ ] Code profiled before optimization -- [ ] Performance-critical code documented diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md index 1f7df8d..b64021a 100644 --- a/.github/instructions/security.instructions.md +++ b/.github/instructions/security.instructions.md @@ -6,220 +6,147 @@ applyTo: '**' ## Core Principles -1. **Validate at the edge** — Validate request shape and value constraints on all external inputs. -2. **Sanitize untrusted content** — Sanitize user-provided markup and normalize text input. -3. **Verify signatures** — Verify signatures for all inbound webhooks or callbacks. -4. **Use safe identifier conversion** — Parse and validate untrusted IDs with strict checks and try/catch. -5. **Fail securely** — Log internal details server-side and return generic client errors. -6. **Least privilege** — Restrict access with explicit authorization policies. -7. **Secrets in environment only** — Validate required secrets at startup and never hardcode credentials. - -## Input Validation - -**Every endpoint should validate request body, params, and query values:** - -```javascript -// Good: Validate request before business logic -const schema = { - type: 'object', - required: ['email', 'password'], - properties: { - email: { type: 'string', format: 'email' }, - password: { type: 'string', minLength: 8 }, - }, -}; - -function createUserHandler(request, response) { - const { valid, errors } = validate(schema, request.body); - if (!valid) { - return response.status(400).json({ error: 'Invalid request payload' }); - } - - const { email, password } = request.body; - return response.status(201).json({ email }); -} - -// Bad: No validation -function createUserHandlerUnsafe(request, response) { - const { email, password } = request.body; // Unsafe - return response.status(201).json({ email, password }); -} +1. **Never expose data in errors** — Error messages must not contain the + original input payload. Use type labels or error categories only. +2. **Validate inputs at library boundaries** — Check that inputs match expected + types before processing. Fail fast with a typed error. +3. **Guard against ReDoS** — Regex patterns that process untrusted input must + avoid catastrophic backtracking. Test patterns against worst-case inputs. +4. **Handle circular references safely** — When recursing into objects, track + visited nodes to prevent infinite loops and stack overflows. +5. **Validate custom patterns** — User-provided pattern strings are untrusted + input. Validate or escape before passing to `RegExp`. +6. **No hardcoded credentials** — Never commit tokens, keys, or secrets to + source. Read secrets from environment variables. +7. **Minimal dependency surface** — Keep dependencies minimal. Audit for known + vulnerabilities regularly. + +## Data Safety in Errors + +**Never include the original value in thrown errors or error details:** + +```typescript +// Bad: exposes the payload +throw new Error(`Cannot sanitize value: ${JSON.stringify(data)}`); + +// Good: only exposes the type +throw new DataSanitizationError( + `Cannot sanitize value of type: ${getInputType(data)}`, +); ``` -## Security Headers - -**Set defensive security headers for all HTTP responses:** +**Use type labels, not values, in all diagnostic output:** -```javascript -function applySecurityHeaders(response) { - response.setHeader('X-Content-Type-Options', 'nosniff'); - response.setHeader('X-Frame-Options', 'DENY'); - response.setHeader('Referrer-Policy', 'no-referrer'); - response.setHeader('Content-Security-Policy', "default-src 'self'"); +```typescript +function getInputType(data: unknown): string { + if (data === null) return 'null'; + if (Array.isArray(data)) return 'array'; + return typeof data; } ``` -## Safe Identifier Handling +## Input Validation at Library Boundaries -**Always validate identifier format before database access:** - -```javascript -function parseStrictId(id) { - if (!/^[a-f0-9]{24}$/i.test(id)) { - throw new Error('Invalid ID format'); - } - return id.toLowerCase(); -} +**Check types before processing and throw typed errors early:** -function findById(id, repository) { - try { - const safeId = parseStrictId(id); - return repository.findOne({ id: safeId }); - } catch { - return null; +```typescript +// Good: validate before recursing +function sanitizeObject(data: unknown): unknown { + if (data === null || typeof data !== 'object') { + throw new DataSanitizationError( + `Expected object, got ${getInputType(data)}`, + ); } + return processObject(data); } ``` -## Webhook Signature Verification - -**Always verify webhook signatures before processing payloads:** - -```javascript -import crypto from 'node:crypto'; - -function verifyWebhookSignature({ payload, signature, secret }) { - const expected = crypto - .createHmac('sha256', secret) - .update(payload) - .digest('hex'); - return crypto.timingSafeEqual( - Buffer.from(expected), - Buffer.from(signature || ''), - ); -} +## ReDoS Protection -function handleWebhook(request, response, secret) { - const signature = request.headers['x-signature']; - const valid = verifyWebhookSignature({ - payload: request.rawBody, - signature, - secret, - }); +**Avoid patterns with nested quantifiers or overlapping alternations on +untrusted input:** - if (!valid) { - return response.status(401).json({ error: 'Invalid signature' }); - } +```typescript +// Risky: nested quantifiers can cause catastrophic backtracking +const unsafe = /^(a+)+$/; - return response.status(200).json({ ok: true }); -} +// Safe: linear patterns with clear boundaries +const safe = /^a+$/; ``` -## Authentication and Authorization - -**Protect private routes and authorize by capability/role:** +**Test custom patterns against long repeated inputs before using them:** -```javascript -function requireAuth(request) { - if (!request.user) { - throw new Error('Unauthorized'); +```typescript +// Validate performance before accepting a user pattern +function benchmarkPattern(pattern: RegExp, worstCase: string): void { + const start = Date.now(); + pattern.test(worstCase); + if (Date.now() - start > 100) { + throw new Error('Pattern too slow on worst-case input'); } } - -function requirePermission(user, permission) { - if (!user.permissions.includes(permission)) { - throw new Error('Forbidden'); - } -} - -function createAdminUser(request, response) { - requireAuth(request); - requirePermission(request.user, 'users:create'); - return response.status(201).json({ ok: true }); -} ``` -## Data Protection - -### Rate Limiting +## Circular Reference Safety -```javascript -function createRateLimiter({ max, windowMs }) { - const hits = new Map(); +**Track visited nodes with a `WeakSet` when recursing into objects:** - return function allow(ip, now = Date.now()) { - const slot = hits.get(ip) || []; - const recent = slot.filter((ts) => now - ts < windowMs); - if (recent.length >= max) return false; - recent.push(now); - hits.set(ip, recent); - return true; - }; +```typescript +// Bad: unbounded recursion +function recurse(obj: Record): void { + for (const key of Object.keys(obj)) { + recurse(obj[key] as Record); + } } -``` -### Input Sanitization - -**Sanitize untrusted user content before persistence or rendering:** - -```javascript -function sanitizeInput(value) { - if (typeof value === 'string') { - return value.replace(/[<>]/g, ''); - } - if (Array.isArray(value)) { - return value.map(sanitizeInput); - } - if (value && typeof value === 'object') { - const next = {}; - for (const [key, item] of Object.entries(value)) { - next[key] = sanitizeInput(item); +// Good: circular reference guard +function recurse(obj: Record, seen = new WeakSet()): void { + if (seen.has(obj)) return; + seen.add(obj); + for (const key of Object.keys(obj)) { + const val = obj[key]; + if (val && typeof val === 'object') { + recurse(val as Record, seen); } - return next; } - return value; } ``` -### Environment Variables and Secrets +## Custom Pattern Validation -**Define and validate all required environment variables at startup:** +**Treat user-provided pattern strings as untrusted. Validate before use:** -```javascript -const requiredEnv = ['JWT_SIGNING_KEY', 'WEBHOOK_SIGNING_SECRET']; +```typescript +// Bad: user string passed directly to RegExp +const pattern = new RegExp(userInput); -function validateEnvironment(env = process.env) { - for (const key of requiredEnv) { - if (!env[key] || env[key].trim() === '') { - throw new Error(`Missing required environment variable: ${key}`); - } +// Good: validate the pattern string first +function validatePatternString(input: string): void { + if (!/^[a-z][a-z0-9_]*$/i.test(input)) { + throw new DataSanitizationError(`Invalid pattern: ${input}`); } } ``` -## Error Handling +## Environment Variables and Secrets -**Never expose internal details to external clients:** +**Read secrets from environment variables. Never hardcode them:** -```javascript -try { - await riskyOperation(); -} catch (error) { - logger.error({ error }, 'Operation failed'); - return response.status(500).json({ error: 'Internal server error' }); -} +```typescript +// Bad: hardcoded secret +const apiKey = 'sk-abc123'; + +// Good: read from environment +const apiKey = process.env.API_KEY; +if (!apiKey) throw new Error('Missing required env var: API_KEY'); ``` ## Checklist -- [ ] Input validation on all external inputs -- [ ] Authentication and authorization on protected routes -- [ ] Rate limiting on externally accessible APIs -- [ ] Security headers configured -- [ ] Error responses avoid internal details -- [ ] Injection/XSS mitigations in place -- [ ] Webhook signatures verified before processing -- [ ] ID parsing and conversion wrapped in strict validation -- [ ] Untrusted input sanitized before storage/rendering -- [ ] Secrets loaded only from environment variables -- [ ] No sensitive data in logs +- [ ] No original data values in error messages or details +- [ ] Inputs validated at library boundaries before processing +- [ ] Regex patterns tested for ReDoS on worst-case inputs +- [ ] Circular references handled with `WeakSet` guards +- [ ] User-provided pattern strings validated before `RegExp` construction +- [ ] No hardcoded tokens, keys, or secrets in source +- [ ] Dependencies audited for known vulnerabilities diff --git a/.gitignore b/.gitignore index 66c95be..947bf75 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ typings/ .envrc dist/ tmp/ + +# claude +.claude/settings.local.json +.claude/worktrees/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..600297e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,34 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development workflow + +See [docs/development.md](docs/development.md) for setup, build, test, lint, planning, PR/commit conventions, and the release process. + +To run a single test file: `yarn vitest run test/matchers.test.ts` + +## Architecture + +This is a TypeScript library that sanitizes sensitive data in objects and strings. The public API is a single function, `sanitizeData`, exported from `src/index.ts`. + +**Data flow:** + +- **String input** → `stringReplacer` applies regex matchers pattern-by-pattern across the string +- **Object/array input** → `objectReplacer` recursively walks the structure, matching keys by name (no JSON round-trip). Non-plain object instances (custom prototypes) are preserved without modification. +- **Null input** → stringified, processed as a string, then parsed back + +**Key modules:** + +- `src/matchers.ts` — Three built-in `DataSanitizationMatcher` factories (`jsonMatcher`, `escapedJsonMatcher`, `formEncodedMatcher`). Each takes a pattern string and optional `remove` flag and returns a `RegExp`. Custom matchers must produce a global, case-insensitive regex using capture groups `$1`/`$2` for value replacement. +- `src/replacers.ts` — `stringReplacer` and `objectReplacer`. String replacer iterates all (pattern × matcher) combinations. Object replacer builds `RegExp` key matchers once, then recurses with a `WeakSet` to detect circular references. +- `src/constants.ts` — Default field-name patterns (`apikey`, `api_key`, `password`, `secret`, `token`) and default mask (`**********`). +- `src/types.ts` — All exported TypeScript types (`DataSanitizationMatcher`, `DataSanitizationReplacer`, `DataSanitizationReplacerOptions`, etc.). +- `src/errors.ts` — `DataSanitizationError` with a `details` property; error details never include the original input payload. + +## Conventions + +@.ai/instructions/code-complexity.md +@.ai/instructions/comments.md +@.ai/instructions/jsdoc.md +@.ai/instructions/unit-tests.md From ae2a3afdf163d02eeddd6795f80333c4b1357a92 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Wed, 20 May 2026 23:36:59 -0400 Subject: [PATCH 02/10] feat: create .ai/ shared layer and migrate instruction/skill content Co-Authored-By: Claude Sonnet 4.6 --- .ai/README.md | 30 ++++ .ai/instructions/code-complexity.md | 81 ++++++++++ .ai/instructions/code-review.md | 94 ++++++++++++ .ai/instructions/comments.md | 61 ++++++++ .ai/instructions/jsdoc.md | 132 +++++++++++++++++ .ai/instructions/performance.md | 63 ++++++++ .ai/instructions/plan-writing.md | 61 ++++++++ .ai/instructions/security.md | 221 ++++++++++++++++++++++++++++ .ai/instructions/unit-tests.md | 152 +++++++++++++++++++ .ai/skills/plan-writing.md | 37 +++++ 10 files changed, 932 insertions(+) create mode 100644 .ai/README.md create mode 100644 .ai/instructions/code-complexity.md create mode 100644 .ai/instructions/code-review.md create mode 100644 .ai/instructions/comments.md create mode 100644 .ai/instructions/jsdoc.md create mode 100644 .ai/instructions/performance.md create mode 100644 .ai/instructions/plan-writing.md create mode 100644 .ai/instructions/security.md create mode 100644 .ai/instructions/unit-tests.md create mode 100644 .ai/skills/plan-writing.md diff --git a/.ai/README.md b/.ai/README.md new file mode 100644 index 0000000..a5631a0 --- /dev/null +++ b/.ai/README.md @@ -0,0 +1,30 @@ +# .ai — Shared AI Customization + +Single source of truth for AI customization content used by both GitHub Copilot +and Claude Code. Each AI system has its own thin wrapper files that reference +back here. + +## Structure + +- `instructions/` — Auto-loaded coding standards and guidelines. Maps to + Copilot's `.github/instructions/` and Claude Code's `CLAUDE.md @imports`. +- `skills/` — Reusable workflows invokable by name. Maps to Copilot's + `.github/skills/` and Claude Code's `.claude/skills/`. +- `prompts/` — User-invokable chat prompts (created on demand). Maps to + Copilot's `.github/prompts/` and Claude Code's `.claude/skills/`. + +## Adding new content + +Use the maintenance skills to create new items — they auto-detect which AI +systems are active and create the appropriate thin wrappers: + +- `/ai-add-instruction` — new auto-loaded instruction +- `/ai-add-prompt` — new user-invokable prompt +- `/ai-add-skill` — new reusable workflow skill + +## Future extraction + +The `ai-add-*` maintenance skills in `skills/` are candidates for extraction +into a Claude plugin and Copilot user-level skills (`~/.copilot/skills/`) once +they stabilize. When extracted, remove them from here and from the thin wrappers +in `.github/skills/` and `.claude/skills/`. diff --git a/.ai/instructions/code-complexity.md b/.ai/instructions/code-complexity.md new file mode 100644 index 0000000..7c5ec05 --- /dev/null +++ b/.ai/instructions/code-complexity.md @@ -0,0 +1,81 @@ +# Code Complexity Guidelines + +## Core Principles + +1. Functions should be small, focused, and do one thing well +2. Prefer readability over strict metrics +3. Functions should be pure when possible +4. Avoid side effects and global state dependencies + +## Hard Limits + +- **Max 3 parameters** — use object parameter for >3 params or boolean flags +- **Max 3 nesting levels** — use early returns and guard clauses +- **Target <30 lines per function** — longer OK if cohesive and clear +- **Target cyclomatic complexity <15** — higher OK if logic is simple and + related + +## Examples and Patterns + +**Use early returns to reduce nesting:** + +```javascript +// Bad: Deep nesting +function process(order) { + if (order.isValid) { + if (order.items.length > 0) { + if (order.customer.isActive) { + return doWork(order); + } + } + } + return null; +} + +// Good: Guard clauses +function process(order) { + if (!order.isValid) return null; + if (order.items.length === 0) return null; + if (!order.customer.isActive) return null; + return doWork(order); +} +``` + +**Use object parameters for related data:** + +```javascript +// Bad: Too many params +function createUser(name, email, age, address, phone) {} + +// Good: Grouped params +function createUser(userData) { + const { name, email, age, address, phone } = userData; +} +``` + +**Extract complex logic into focused functions:** + +```javascript +// Bad: Multiple responsibilities +function processUserData(user) { + validateUser(user); + updateDatabase(user); + sendWelcomeEmail(user); + createUserProfile(user); +} + +// Good: Composed from smaller functions +function processUserData(user) { + validateUser(user); + const dbUser = updateDatabase(user); + notifyUser(dbUser); +} +``` + +## Checklist + +- [ ] Single responsibility +- [ ] ≤3 parameters +- [ ] ≤3 nesting levels +- [ ] Clear, descriptive names +- [ ] No global state dependencies diff --git a/.ai/instructions/code-review.md b/.ai/instructions/code-review.md new file mode 100644 index 0000000..da19642 --- /dev/null +++ b/.ai/instructions/code-review.md @@ -0,0 +1,94 @@ +# Copilot Code Review Instructions + +Important: This file is intended solely for Copilot and AI agents performing +code reviews on GitHub. It should not be used by coding assistants or agents +when implementing code. + +## Purpose + +Reviews should focus on new or modified code in the PR, not pre-existing issues. +Comments should be actionable and specific to what changed. + +## Core Principles + +1. **Review Only New or Modified Code** + - Do not comment on issues that existed before the current changes (e.g., + file length, missing JSDoc, legacy patterns). + - Focus feedback on code that was added, changed, or deleted in the pull + request. + +2. **No Retroactive Enforcement** + - Don't flag existing violations unless the change introduces a new issue or + significantly worsens what's already there. + - Example: A file already has 400 lines and the PR adds 10 more. Don't + complain about file length. If the PR adds 100+ lines, suggest splitting + only the new code. + +3. **Actionable Feedback** + - Be specific about what changed. + - Skip generic complaints about the codebase. + +4. **Respect Project-Specific Exceptions** + - If project instructions allow exceptions or best-effort patterns, don't + enforce stricter rules. + +5. **No Blame for Existing Code** + - Do not attribute responsibility for existing issues to the current author + or PR. + +## Examples + +- **File Size**: + If a file is already over a recommended line limit and the PR adds more lines, + do not flag the file size. Only suggest splitting if the change introduces a + new violation. + +- **JSDoc/Comments**: + Only require JSDoc for new functions or modified functions that already have + JSDoc. Don't force adding JSDoc to legacy code just because it was touched. + +- **Tests**: + Require tests for new features or changes, not for untouched existing code. + +- **Naming/Patterns**: + Flag naming or pattern issues only in new/modified code. + +- **Security**: + Always flag security vulnerabilities in new or modified code, regardless of + existing code state. + +## Reference to Other Instruction Files + +Other instruction files (`code-complexity`, `comments`, `jsdoc`, `performance`, +`security`, `unit-tests`) are primarily for coding agents implementing changes. +Use them as guidelines during reviews, but apply standards only to new or +modified code — don't flag pre-existing issues unless the change makes them +worse. + +## Handling Slight Regressions (Suggestion vs Requirement) + +When a change slightly worsens an existing issue, favor suggestions over +requirements unless the regression is serious: + +- **Critical or security-sensitive**: Require fixes for security + vulnerabilities, data leaks, or critical bugs before merging. +- **High-impact performance or correctness**: Require a fix or follow-up plan + (open an issue, link to PR). +- **Low-impact or cosmetic**: Suggest improvements (styling, file length, + non-critical JSDoc) but mark as optional. +- **Easy to fix**: If the fix is quick, request it directly rather than + deferring. + +When in doubt, suggest rather than require. Explain why for requirements; offer +examples for suggestions. Link to instruction files when relevant (`jsdoc`, +`performance`, `code-complexity`). + +## Checklist + +- [ ] Feedback is limited to new/changed code +- [ ] No comments on pre-existing issues unless made worse by the PR +- [ ] Suggestions are actionable for the current author +- [ ] No blame or requests to fix existing code +- [ ] Project-specific instructions and exceptions are respected +- [ ] Slight regressions handled appropriately (suggestion vs requirement) +- [ ] Security issues always flagged, regardless of existing code state diff --git a/.ai/instructions/comments.md b/.ai/instructions/comments.md new file mode 100644 index 0000000..24a88ac --- /dev/null +++ b/.ai/instructions/comments.md @@ -0,0 +1,61 @@ +# Comments and Documentation + +## Core Principles + +1. **Default: no comment** — Add comments only when absolutely necessary. +2. **Only why, never what** — Explain the reason or decision, not what the code + does +3. **Never comment about removed/refactored/changed code** — Comments describe + present code only +4. **Never mention LLM actions** — Avoid phrases like "imported function", "removed code", + "refactored", etc. +5. **Code should be self-documenting** — Prefer making code clearer over adding comments. + +## When Comments Are Allowed + +Add a why-comment only when the reader would genuinely struggle to understand +the decision without it: + +- **Non-obvious decisions**: Why this approach over alternatives +- **Edge cases**: Why special handling is needed +- **Performance trade-offs**: Why we accept certain costs +- **Magic numbers**: Why this specific value + +## Examples and Patterns + +```javascript +// BAD: Describing what code does +// Loop through items and sum prices +const total = items.reduce((sum, item) => sum + item.price, 0); + +// BAD: Redundant with function name +// Calculate the total +function calculateTotal() {} + +// BAD: Describing removed code +// Removed the old validation logic + +// BAD: Mentioning refactoring +// Refactored to use new helper function + +// BAD: LLM action commentary +// Imported the helper function +// Updated this section per requirements + +// GOOD: Explains WHY +// Using reduce instead of forEach to avoid mutation +const total = items.reduce((sum, item) => sum + item.price, 0); + +// GOOD: Explains non-obvious decision +// 0.7 threshold balances precision/recall based on historical data analysis +const DETECTION_THRESHOLD = 0.7; +``` + +## Checklist + +- [ ] No comment added unless truly necessary +- [ ] Comment explains why, never what +- [ ] No mention of removed/refactored/changed code +- [ ] No LLM action descriptions ("imported", "removed", "refactored") +- [ ] Comment is about present behavior only +- [ ] Could not make code clearer instead of commenting diff --git a/.ai/instructions/jsdoc.md b/.ai/instructions/jsdoc.md new file mode 100644 index 0000000..85ecd03 --- /dev/null +++ b/.ai/instructions/jsdoc.md @@ -0,0 +1,132 @@ +# JSDoc Documentation Standards + +## Core Principles + +1. **Every function must have JSDoc** — No exceptions. +2. **No inline type definitions** — Use typedefs or imported types. +3. **Import types from packages** — Use `import('package').Type` syntax +4. **Include function description** — Clear explanation of what it does +5. **Add @example for functions with I/O** — Show actual usage patterns +6. **Add @throws for each error type** — Document every throw statement with + specific error condition + +## Type Definition Rules + +**Use `import()` for external types:** + +```javascript +/** + * @typedef {import('node:fs').Stats} FileStats + * @typedef {import('node:http').IncomingHttpHeaders} IncomingHttpHeaders + * @typedef {import('./types.js').UserRecord} UserRecord + */ +``` + +**Define custom types as typedefs:** + +```javascript +/** + * @typedef {'low'|'medium'|'high'} Priority + * @typedef {Object} QueryFilters + * @property {string} [ownerId] - Owner identifier + * @property {string} [status] - Optional status filter + * @property {number} [page=1] - Page number + * @property {number} [limit=100] - Items per page + */ +``` + +**Use `[...]` for arrays, not `Array<...>`:** + +```javascript +@param {[number, number][]} ranges - Array of [min, max] pairs +@returns {string[]} Array of user IDs +``` + +## Complete Function Documentation + +Every function should include: + +1. **Description** — What the function does +2. **@param** — All parameters with imported/typedef types +3. **@returns** — Return type (use `Promise` for async) +4. **@throws** — Document error conditions +5. **@example** — Usage example for functions with parameters or return values + +```javascript +/** + * Retrieves records with optional pagination and filtering. + * Adds computed metadata needed by downstream consumers. + * + * @param {import('./repository.js').RecordRepository} repository - Data repository + * @param {QueryFilters} filters - Filter criteria + * @param {import('./logger.js').Logger} logger - Logger instance + * @returns {Promise} Query results with metadata + * @throws {Error} When data retrieval fails + * @throws {Error} When response shaping fails + * + * @example + * // Get records with pagination + * const result = await getRecords(repository, { + * ownerId: 'user-123', + * page: 1, + * limit: 50 + * }, logger) + * + * @example + * // Get records within a date range + * const result = await getRecords(repository, { + * ownerId: 'user-456', + * start_date: '2025-01-01T00:00:00Z', + * end_date: '2025-01-31T23:59:59Z' + * }, logger) + */ +async function getRecords(repository, filters, logger) { + // Implementation +} +``` + +## Common Patterns + +**Optional parameters with defaults:** + +```javascript +@param {number} [page=1] - Page number +@param {boolean} [include_counts=false] - Include counts +``` + +**Multiple type options:** + +```javascript +@param {string|number} id - User ID +@returns {User|null} User object or null if not found +``` + +**Destructured parameters:** + +```javascript +/** + * @param {Object} options + * @param {string} options.ownerId - Owner identifier + * @param {boolean} [options.includeCounts] - Include counts + */ +function process({ ownerId, includeCounts }) {} +``` + +**Callback/function types:** + +```javascript +@param {(error: Error|null, result: any) => void} callback +``` + +## Checklist + +When documenting types, verify: + +- [ ] Types match actual data passed to function +- [ ] Return types match actual returned data +- [ ] Optional params marked with `[param]` or `[param=default]` +- [ ] Nullable types marked with `|null` or `|undefined` +- [ ] Union types used for multiple accepted types +- [ ] No inline `Object` — use typedef or imported type +- [ ] Array element types specified: `[ElementType]` +- [ ] Async functions return `Promise` diff --git a/.ai/instructions/performance.md b/.ai/instructions/performance.md new file mode 100644 index 0000000..5a9afda --- /dev/null +++ b/.ai/instructions/performance.md @@ -0,0 +1,63 @@ +# Performance Guidelines + +## Core Principles + +1. **Readability first** — Optimize only when needed +2. **Profile before optimizing** — Measure, don't guess +3. **Document performance-critical code** — Explain why optimizations exist +4. **Split large files** — Break into focused modules when exceeding 300 lines + +## Memory Management + +**Always clean up resources:** + +```javascript +// Good: Cleanup event listeners +function startWatcher(target) { + const onChange = () => { + /* ... */ + }; + target.addEventListener('change', onChange); + return () => target.removeEventListener('change', onChange); +} + +// Good: Cleanup timers +const timer = setInterval(runWork, 1000); +clearInterval(timer); +``` + +## Compute Optimization + +**Cache expensive calculations when inputs are stable:** + +```javascript +const cache = new Map(); + +function computeDigest(input) { + if (cache.has(input)) return cache.get(input); + const result = expensiveOperation(input); + cache.set(input, result); + return result; +} +``` + +## Code Splitting + +**Lazy load heavy modules:** + +```javascript +async function loadAnalyzer() { + const { analyze } = await import('./analyzer.js'); + return analyze; +} +``` + +## Checklist + +- [ ] Event listeners and timers cleaned up +- [ ] Large objects released when no longer needed +- [ ] Expensive calculations cached where beneficial +- [ ] Heavy modules lazy loaded where practical +- [ ] Files split when exceeding 300 lines +- [ ] Code profiled before optimization +- [ ] Performance-critical code documented diff --git a/.ai/instructions/plan-writing.md b/.ai/instructions/plan-writing.md new file mode 100644 index 0000000..0ca6178 --- /dev/null +++ b/.ai/instructions/plan-writing.md @@ -0,0 +1,61 @@ +# Plan Writing Standards + +## When to Write a Plan + +Write a plan document before implementing any non-trivial change: new features, +architectural decisions, workflow additions, or anything that benefits from a +recorded rationale. Trivial fixes (typos, one-line patches) do not need a plan. + +## Naming Convention + +Plans are named sequentially: `NNN-short-description.md` + +- `NNN` is a zero-padded three-digit number starting at `000` +- Use lowercase kebab-case for the description +- The number is assigned at creation time and never changes + +Examples: `000-plan-system.md`, `001-coverage-tracking.md` + +## Required Sections + +Every plan document must include the following sections in this order: + +### Title + +A single `#` heading that names the plan. Should match the filename description. + +### Approach + +One paragraph explaining what is being done and why at a high level. + +### Pre-implementation + +Any manual steps that must be completed before code changes begin (creating +external accounts, secrets, branches, issues). Omit this section if there are +none. + +### Steps + +Numbered list of implementation steps. Each step should name the file being +created or modified and describe what changes. + +### Relevant Files + +A flat list of every file touched, with a one-line note on whether it is new +or updated and what it does. + +### Verification + +How to confirm the implementation is correct and complete. + +### Decisions + +Key choices made during planning and the rationale behind them. This is the +most important section for future reference — capture the why, not just the +what. + +## Style + +- Write in plain present tense ("Add X", "Update Y") +- Be specific about file paths +- Decisions should explain trade-offs, not just state what was chosen diff --git a/.ai/instructions/security.md b/.ai/instructions/security.md new file mode 100644 index 0000000..621293d --- /dev/null +++ b/.ai/instructions/security.md @@ -0,0 +1,221 @@ +# Security Guidelines + +## Core Principles + +1. **Validate at the edge** — Validate request shape and value constraints on all external inputs. +2. **Sanitize untrusted content** — Sanitize user-provided markup and normalize text input. +3. **Verify signatures** — Verify signatures for all inbound webhooks or callbacks. +4. **Use safe identifier conversion** — Parse and validate untrusted IDs with strict checks and try/catch. +5. **Fail securely** — Log internal details server-side and return generic client errors. +6. **Least privilege** — Restrict access with explicit authorization policies. +7. **Secrets in environment only** — Validate required secrets at startup and never hardcode credentials. + +## Input Validation + +**Every endpoint should validate request body, params, and query values:** + +```javascript +// Good: Validate request before business logic +const schema = { + type: 'object', + required: ['email', 'password'], + properties: { + email: { type: 'string', format: 'email' }, + password: { type: 'string', minLength: 8 }, + }, +}; + +function createUserHandler(request, response) { + const { valid, errors } = validate(schema, request.body); + if (!valid) { + return response.status(400).json({ error: 'Invalid request payload' }); + } + + const { email, password } = request.body; + return response.status(201).json({ email }); +} + +// Bad: No validation +function createUserHandlerUnsafe(request, response) { + const { email, password } = request.body; // Unsafe + return response.status(201).json({ email, password }); +} +``` + +## Security Headers + +**Set defensive security headers for all HTTP responses:** + +```javascript +function applySecurityHeaders(response) { + response.setHeader('X-Content-Type-Options', 'nosniff'); + response.setHeader('X-Frame-Options', 'DENY'); + response.setHeader('Referrer-Policy', 'no-referrer'); + response.setHeader('Content-Security-Policy', "default-src 'self'"); +} +``` + +## Safe Identifier Handling + +**Always validate identifier format before database access:** + +```javascript +function parseStrictId(id) { + if (!/^[a-f0-9]{24}$/i.test(id)) { + throw new Error('Invalid ID format'); + } + return id.toLowerCase(); +} + +function findById(id, repository) { + try { + const safeId = parseStrictId(id); + return repository.findOne({ id: safeId }); + } catch { + return null; + } +} +``` + +## Webhook Signature Verification + +**Always verify webhook signatures before processing payloads:** + +```javascript +import crypto from 'node:crypto'; + +function verifyWebhookSignature({ payload, signature, secret }) { + const expected = crypto + .createHmac('sha256', secret) + .update(payload) + .digest('hex'); + return crypto.timingSafeEqual( + Buffer.from(expected), + Buffer.from(signature || ''), + ); +} + +function handleWebhook(request, response, secret) { + const signature = request.headers['x-signature']; + const valid = verifyWebhookSignature({ + payload: request.rawBody, + signature, + secret, + }); + + if (!valid) { + return response.status(401).json({ error: 'Invalid signature' }); + } + + return response.status(200).json({ ok: true }); +} +``` + +## Authentication and Authorization + +**Protect private routes and authorize by capability/role:** + +```javascript +function requireAuth(request) { + if (!request.user) { + throw new Error('Unauthorized'); + } +} + +function requirePermission(user, permission) { + if (!user.permissions.includes(permission)) { + throw new Error('Forbidden'); + } +} + +function createAdminUser(request, response) { + requireAuth(request); + requirePermission(request.user, 'users:create'); + return response.status(201).json({ ok: true }); +} +``` + +## Data Protection + +### Rate Limiting + +```javascript +function createRateLimiter({ max, windowMs }) { + const hits = new Map(); + + return function allow(ip, now = Date.now()) { + const slot = hits.get(ip) || []; + const recent = slot.filter((ts) => now - ts < windowMs); + if (recent.length >= max) return false; + recent.push(now); + hits.set(ip, recent); + return true; + }; +} +``` + +### Input Sanitization + +**Sanitize untrusted user content before persistence or rendering:** + +```javascript +function sanitizeInput(value) { + if (typeof value === 'string') { + return value.replace(/[<>]/g, ''); + } + if (Array.isArray(value)) { + return value.map(sanitizeInput); + } + if (value && typeof value === 'object') { + const next = {}; + for (const [key, item] of Object.entries(value)) { + next[key] = sanitizeInput(item); + } + return next; + } + return value; +} +``` + +### Environment Variables and Secrets + +**Define and validate all required environment variables at startup:** + +```javascript +const requiredEnv = ['JWT_SIGNING_KEY', 'WEBHOOK_SIGNING_SECRET']; + +function validateEnvironment(env = process.env) { + for (const key of requiredEnv) { + if (!env[key] || env[key].trim() === '') { + throw new Error(`Missing required environment variable: ${key}`); + } + } +} +``` + +## Error Handling + +**Never expose internal details to external clients:** + +```javascript +try { + await riskyOperation(); +} catch (error) { + logger.error({ error }, 'Operation failed'); + return response.status(500).json({ error: 'Internal server error' }); +} +``` + +## Checklist + +- [ ] Input validation on all external inputs +- [ ] Authentication and authorization on protected routes +- [ ] Rate limiting on externally accessible APIs +- [ ] Security headers configured +- [ ] Error responses avoid internal details +- [ ] Injection/XSS mitigations in place +- [ ] Webhook signatures verified before processing +- [ ] ID parsing and conversion wrapped in strict validation +- [ ] Untrusted input sanitized before storage/rendering +- [ ] Secrets loaded only from environment variables +- [ ] No sensitive data in logs diff --git a/.ai/instructions/unit-tests.md b/.ai/instructions/unit-tests.md new file mode 100644 index 0000000..c0404fb --- /dev/null +++ b/.ai/instructions/unit-tests.md @@ -0,0 +1,152 @@ +# Unit Testing Standards (Vitest) + +**Applies to:** JavaScript/TypeScript tests. Other language test guidance is out of scope. + +## Core Principles + +1. Use Arrange, Act, and Assert comments for non-trivial tests; add Revert only + when the test performs cleanup +2. Aim for 100% coverage; use `/* istanbul ignore next */` with justification +3. BDD format: `describe()` blocks + `it()` (never use `test()`) +4. Test titles should start with `should` and describe behavior +5. Location: follow the repository's established test layout and naming patterns + +## Test Structure + +```javascript +import { describe, it, expect, beforeEach } from 'vitest'; +import { functionToTest } from './moduleToTest'; + +describe('moduleToTest.js', () => { + describe('functionToTest', () => { + it('should do something specific when given certain input', () => { + // Arrange + const input = 'test'; + + // Act + const result = functionToTest(input); + + // Assert + expect(result).toBe('expected'); + }); + }); +}); +``` + +## Coverage & Mocking + +**Coverage:** Use `/* istanbul ignore next */` only when necessary (error +boundaries, third-party code, platform-specific, E2E-tested UI). Add comment +explaining why. + +**Mocking:** Avoid when possible. If needed, mock at lowest level and document +why. Prefer refactoring for testability: + +```javascript +// Before: requires mocking Date +function getCurrentTime() { + return new Date().toISOString(); +} + +// After: testable via dependency injection +function getCurrentTime(date = new Date()) { + return date.toISOString(); +} +``` + +## Test Execution + +After any test file change (new tests, edits, refactors): + +1. Run the modified test file +2. Fix failures +3. Repeat until green + +Do not consider test changes complete until tests pass. + +## BDD Format Requirements + +- Use `describe()` for context and unit under test +- Use `it()` for test cases (never use `test()`) +- Titles should start with `should` + observable behavior +- One behavior per test when possible + +```javascript +describe('Calculator', () => { + describe('add', () => { + it('should return sum of two positive numbers', () => {}); + it('should handle negative numbers correctly', () => {}); + it('should throw error for non-numeric input', () => {}); + }); +}); +``` + +## AAA and Revert Comments + +Use in every non-trivial test: + +- `// Arrange` — setup/given +- `// Act` — when +- `// Assert` — then + +Use only when cleanup is actually performed: + +- `// Revert` — cleanup local test state, mocks, timers, files, or other side + effects not handled by `afterEach` + +Simple tests with obvious steps may omit these. + +## Example Test + +```javascript +import { describe, it, expect, beforeEach } from 'vitest'; +import { processRanges } from './labResults'; + +describe('labResults.js', () => { + describe('processRanges', () => { + let ranges, value, options; + + beforeEach(() => { + // Arrange + ranges = { + optimal: [[0, 10]], + warning: [[11, 20]], + danger: [[21, 30]], + }; + value = 15; + options = {}; + }); + + it('should return correct range info for value in warning range', () => { + // Act + const result = processRanges(ranges, value, options); + + // Assert + expect(result.activeRange.type).toBe('warning'); + expect(result.percent).toBeGreaterThan(0); + expect(result.percent).toBeLessThan(100); + }); + + it('should handle null ranges gracefully', () => { + // Arrange + ranges = null; + + // Act + const result = processRanges(ranges, value, options); + + // Assert + expect(result.noRanges).toBe(true); + expect(result.percent).toBe(50); + }); + }); +}); +``` + +## Checklist + +- [ ] Tests use `describe()` and `it()` +- [ ] Test titles start with `should` +- [ ] Arrange, Act, and Assert comments are present in non-trivial tests +- [ ] Revert comments are present only when the test performs cleanup +- [ ] Coverage exceptions include justification comments +- [ ] Modified tests are run and passing diff --git a/.ai/skills/plan-writing.md b/.ai/skills/plan-writing.md new file mode 100644 index 0000000..18d488e --- /dev/null +++ b/.ai/skills/plan-writing.md @@ -0,0 +1,37 @@ +# Plan Writing + +## When to Use + +- Starting any non-trivial implementation (feature, workflow, architectural change) +- When asked to "make a plan" or "write a plan" for a change +- Before creating a branch or issue for significant work + +## Format Rules + +Follow the plan format rules in `.ai/instructions/plan-writing.md`. + +> **Claude Code:** `@.ai/instructions/plan-writing.md` +> **GitHub Copilot:** `#file:../../.ai/instructions/plan-writing.md` + +## Workflow + +1. **Determine the next plan number** — list `docs/plans/` and find the + highest existing `NNN` prefix; increment by one (pad to three digits) + +2. **Gather context** — read relevant source files, existing plans, and any + linked issues to understand scope before writing + +3. **Draft the plan** — create `docs/plans/NNN-description.md` using + `docs/plans/TEMPLATE.md` as the skeleton; fill every required section + +4. **Decisions first** — if the user has already discussed trade-offs in the + conversation, capture them in the Decisions section before they are lost + +5. **Confirm before saving** — present the draft to the user for review if the + scope is large or ambiguous; save directly for straightforward plans + +## Naming + +Use the filename description to summarize the change in two to four words, +lowercase kebab-case. The number is assigned at creation time and never +changes. From 191e66fbc485e4b5657cc0e58da91297f96abd85 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Wed, 20 May 2026 23:44:37 -0400 Subject: [PATCH 03/10] fix: clarify cross-reference syntax examples in plan-writing skill --- .ai/skills/plan-writing.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.ai/skills/plan-writing.md b/.ai/skills/plan-writing.md index 18d488e..f6fa3ac 100644 --- a/.ai/skills/plan-writing.md +++ b/.ai/skills/plan-writing.md @@ -10,8 +10,11 @@ Follow the plan format rules in `.ai/instructions/plan-writing.md`. -> **Claude Code:** `@.ai/instructions/plan-writing.md` -> **GitHub Copilot:** `#file:../../.ai/instructions/plan-writing.md` +When creating thin wrapper files that reference this instruction, use the +appropriate syntax for each AI system: + +- **Claude Code** thin wrappers: `@.ai/instructions/plan-writing.md` +- **GitHub Copilot** thin wrappers: `#file:../../.ai/instructions/plan-writing.md` ## Workflow From a6846250755276604e0926902a6c2f3176528ba3 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Wed, 20 May 2026 23:47:08 -0400 Subject: [PATCH 04/10] feat: convert Copilot instruction and skill files to thin wrappers --- .../code-complexity.instructions.md | 82 +--------- .../instructions/code-review.instructions.md | 95 +---------- .github/instructions/comments.instructions.md | 62 +------ .../instructions/plan-writing.instructions.md | 62 +------ .github/instructions/security.instructions.md | 149 +---------------- .../instructions/unit-tests.instructions.md | 153 +----------------- .github/skills/plan-writing/SKILL.md | 37 +---- 7 files changed, 7 insertions(+), 633 deletions(-) diff --git a/.github/instructions/code-complexity.instructions.md b/.github/instructions/code-complexity.instructions.md index e346d9b..ae3ed03 100644 --- a/.github/instructions/code-complexity.instructions.md +++ b/.github/instructions/code-complexity.instructions.md @@ -2,84 +2,4 @@ applyTo: '**' --- -# Code Complexity Guidelines - -## Core Principles - -1. Functions should be small, focused, and do one thing well -2. Prefer readability over strict metrics -3. Functions should be pure when possible -4. Avoid side effects and global state dependencies - -## Hard Limits - -- **Max 3 parameters** — use object parameter for >3 params or boolean flags -- **Max 3 nesting levels** — use early returns and guard clauses -- **Target <30 lines per function** — longer OK if cohesive and clear -- **Target cyclomatic complexity <15** — higher OK if logic is simple and - related - -## Examples and Patterns - -**Use early returns to reduce nesting:** - -```javascript -// Bad: Deep nesting -function process(order) { - if (order.isValid) { - if (order.items.length > 0) { - if (order.customer.isActive) { - return doWork(order); - } - } - } - return null; -} - -// Good: Guard clauses -function process(order) { - if (!order.isValid) return null; - if (order.items.length === 0) return null; - if (!order.customer.isActive) return null; - return doWork(order); -} -``` - -**Use object parameters for related data:** - -```javascript -// Bad: Too many params -function createUser(name, email, age, address, phone) {} - -// Good: Grouped params -function createUser(userData) { - const { name, email, age, address, phone } = userData; -} -``` - -**Extract complex logic into focused functions:** - -```javascript -// Bad: Multiple responsibilities -function processUserData(user) { - validateUser(user); - updateDatabase(user); - sendWelcomeEmail(user); - createUserProfile(user); -} - -// Good: Composed from smaller functions -function processUserData(user) { - validateUser(user); - const dbUser = updateDatabase(user); - notifyUser(dbUser); -} -``` - -## Checklist - -- [ ] Single responsibility -- [ ] ≤3 parameters -- [ ] ≤3 nesting levels -- [ ] Clear, descriptive names -- [ ] No global state dependencies +#file:../../.ai/instructions/code-complexity.md diff --git a/.github/instructions/code-review.instructions.md b/.github/instructions/code-review.instructions.md index ddf9b5f..c84bbed 100644 --- a/.github/instructions/code-review.instructions.md +++ b/.github/instructions/code-review.instructions.md @@ -2,97 +2,4 @@ applyTo: '**' --- -# Copilot Code Review Instructions - -Important: This file is intended solely for Copilot and AI agents performing -code reviews on GitHub. It should not be used by coding assistants or agents -when implementing code. - -## Purpose - -Reviews should focus on new or modified code in the PR, not pre-existing issues. -Comments should be actionable and specific to what changed. - -## Core Principles - -1. **Review Only New or Modified Code** - - Do not comment on issues that existed before the current changes (e.g., - file length, missing JSDoc, legacy patterns). - - Focus feedback on code that was added, changed, or deleted in the pull - request. - -2. **No Retroactive Enforcement** - - Don't flag existing violations unless the change introduces a new issue or - significantly worsens what's already there. - - Example: A file already has 400 lines and the PR adds 10 more. Don't - complain about file length. If the PR adds 100+ lines, suggest splitting - only the new code. - -3. **Actionable Feedback** - - Be specific about what changed. - - Skip generic complaints about the codebase. - -4. **Respect Project-Specific Exceptions** - - If project instructions allow exceptions or best-effort patterns, don't - enforce stricter rules. - -5. **No Blame for Existing Code** - - Do not attribute responsibility for existing issues to the current author - or PR. - -## Examples - -- **File Size**: - If a file is already over a recommended line limit and the PR adds more lines, - do not flag the file size. Only suggest splitting if the change introduces a - new violation. - -- **JSDoc/Comments**: - Only require JSDoc for new functions or modified functions that already have - JSDoc. Don't force adding JSDoc to legacy code just because it was touched. - -- **Tests**: - Require tests for new features or changes, not for untouched existing code. - -- **Naming/Patterns**: - Flag naming or pattern issues only in new/modified code. - -- **Security**: - Always flag security vulnerabilities in new or modified code, regardless of - existing code state. - -## Reference to Other Instruction Files - -Other instruction files (`code-complexity`, `comments`, `jsdoc`, `performance`, -`security`, `unit-tests`) are primarily for coding agents implementing changes. -Use them as guidelines during reviews, but apply standards only to new or -modified code — don't flag pre-existing issues unless the change makes them -worse. - -## Handling Slight Regressions (Suggestion vs Requirement) - -When a change slightly worsens an existing issue, favor suggestions over -requirements unless the regression is serious: - -- **Critical or security-sensitive**: Require fixes for security - vulnerabilities, data leaks, or critical bugs before merging. -- **High-impact performance or correctness**: Require a fix or follow-up plan - (open an issue, link to PR). -- **Low-impact or cosmetic**: Suggest improvements (styling, file length, - non-critical JSDoc) but mark as optional. -- **Easy to fix**: If the fix is quick, request it directly rather than - deferring. - -When in doubt, suggest rather than require. Explain why for requirements; offer -examples for suggestions. Link to instruction files when relevant (`jsdoc`, -`performance`, `code-complexity`). - -## Checklist - -- [ ] Feedback is limited to new/changed code -- [ ] No comments on pre-existing issues unless made worse by the PR -- [ ] Suggestions are actionable for the current author -- [ ] No blame or requests to fix existing code -- [ ] Project-specific instructions and exceptions are respected -- [ ] Slight regressions handled appropriately (suggestion vs requirement) -- [ ] Security issues always flagged, regardless of existing code state +#file:../../.ai/instructions/code-review.md diff --git a/.github/instructions/comments.instructions.md b/.github/instructions/comments.instructions.md index ed85ead..2957ead 100644 --- a/.github/instructions/comments.instructions.md +++ b/.github/instructions/comments.instructions.md @@ -2,64 +2,4 @@ applyTo: '**' --- -# Comments and Documentation - -## Core Principles - -1. **Default: no comment** — Add comments only when absolutely necessary. -2. **Only why, never what** — Explain the reason or decision, not what the code - does -3. **Never comment about removed/refactored/changed code** — Comments describe - present code only -4. **Never mention LLM actions** — Avoid phrases like "imported function", "removed code", - "refactored", etc. -5. **Code should be self-documenting** — Prefer making code clearer over adding comments. - -## When Comments Are Allowed - -Add a why-comment only when the reader would genuinely struggle to understand -the decision without it: - -- **Non-obvious decisions**: Why this approach over alternatives -- **Edge cases**: Why special handling is needed -- **Performance trade-offs**: Why we accept certain costs -- **Magic numbers**: Why this specific value - -## Examples and Patterns - -```javascript -// BAD: Describing what code does -// Loop through items and sum prices -const total = items.reduce((sum, item) => sum + item.price, 0); - -// BAD: Redundant with function name -// Calculate the total -function calculateTotal() {} - -// BAD: Describing removed code -// Removed the old validation logic - -// BAD: Mentioning refactoring -// Refactored to use new helper function - -// BAD: LLM action commentary -// Imported the helper function -// Updated this section per requirements - -// GOOD: Explains WHY -// Using reduce instead of forEach to avoid mutation -const total = items.reduce((sum, item) => sum + item.price, 0); - -// GOOD: Explains non-obvious decision -// 0.7 threshold balances precision/recall based on historical data analysis -const DETECTION_THRESHOLD = 0.7; -``` - -## Checklist - -- [ ] No comment added unless truly necessary -- [ ] Comment explains why, never what -- [ ] No mention of removed/refactored/changed code -- [ ] No LLM action descriptions ("imported", "removed", "refactored") -- [ ] Comment is about present behavior only -- [ ] Could not make code clearer instead of commenting +#file:../../.ai/instructions/comments.md diff --git a/.github/instructions/plan-writing.instructions.md b/.github/instructions/plan-writing.instructions.md index 6ba25ca..73e3aac 100644 --- a/.github/instructions/plan-writing.instructions.md +++ b/.github/instructions/plan-writing.instructions.md @@ -2,64 +2,4 @@ applyTo: 'docs/plans/**' --- -# Plan Writing Standards - -## When to Write a Plan - -Write a plan document before implementing any non-trivial change: new features, -architectural decisions, workflow additions, or anything that benefits from a -recorded rationale. Trivial fixes (typos, one-line patches) do not need a plan. - -## Naming Convention - -Plans are named sequentially: `NNN-short-description.md` - -- `NNN` is a zero-padded three-digit number starting at `000` -- Use lowercase kebab-case for the description -- The number is assigned at creation time and never changes - -Examples: `000-plan-system.md`, `001-coverage-tracking.md` - -## Required Sections - -Every plan document must include the following sections in this order: - -### Title - -A single `#` heading that names the plan. Should match the filename description. - -### Approach - -One paragraph explaining what is being done and why at a high level. - -### Pre-implementation - -Any manual steps that must be completed before code changes begin (creating -external accounts, secrets, branches, issues). Omit this section if there are -none. - -### Steps - -Numbered list of implementation steps. Each step should name the file being -created or modified and describe what changes. - -### Relevant Files - -A flat list of every file touched, with a one-line note on whether it is new -or updated and what it does. - -### Verification - -How to confirm the implementation is correct and complete. - -### Decisions - -Key choices made during planning and the rationale behind them. This is the -most important section for future reference — capture the why, not just the -what. - -## Style - -- Write in plain present tense ("Add X", "Update Y") -- Be specific about file paths -- Decisions should explain trade-offs, not just state what was chosen +#file:../../.ai/instructions/plan-writing.md diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md index b64021a..1bed75c 100644 --- a/.github/instructions/security.instructions.md +++ b/.github/instructions/security.instructions.md @@ -2,151 +2,4 @@ applyTo: '**' --- -# Security Guidelines - -## Core Principles - -1. **Never expose data in errors** — Error messages must not contain the - original input payload. Use type labels or error categories only. -2. **Validate inputs at library boundaries** — Check that inputs match expected - types before processing. Fail fast with a typed error. -3. **Guard against ReDoS** — Regex patterns that process untrusted input must - avoid catastrophic backtracking. Test patterns against worst-case inputs. -4. **Handle circular references safely** — When recursing into objects, track - visited nodes to prevent infinite loops and stack overflows. -5. **Validate custom patterns** — User-provided pattern strings are untrusted - input. Validate or escape before passing to `RegExp`. -6. **No hardcoded credentials** — Never commit tokens, keys, or secrets to - source. Read secrets from environment variables. -7. **Minimal dependency surface** — Keep dependencies minimal. Audit for known - vulnerabilities regularly. - -## Data Safety in Errors - -**Never include the original value in thrown errors or error details:** - -```typescript -// Bad: exposes the payload -throw new Error(`Cannot sanitize value: ${JSON.stringify(data)}`); - -// Good: only exposes the type -throw new DataSanitizationError( - `Cannot sanitize value of type: ${getInputType(data)}`, -); -``` - -**Use type labels, not values, in all diagnostic output:** - -```typescript -function getInputType(data: unknown): string { - if (data === null) return 'null'; - if (Array.isArray(data)) return 'array'; - return typeof data; -} -``` - -## Input Validation at Library Boundaries - -**Check types before processing and throw typed errors early:** - -```typescript -// Good: validate before recursing -function sanitizeObject(data: unknown): unknown { - if (data === null || typeof data !== 'object') { - throw new DataSanitizationError( - `Expected object, got ${getInputType(data)}`, - ); - } - return processObject(data); -} -``` - -## ReDoS Protection - -**Avoid patterns with nested quantifiers or overlapping alternations on -untrusted input:** - -```typescript -// Risky: nested quantifiers can cause catastrophic backtracking -const unsafe = /^(a+)+$/; - -// Safe: linear patterns with clear boundaries -const safe = /^a+$/; -``` - -**Test custom patterns against long repeated inputs before using them:** - -```typescript -// Validate performance before accepting a user pattern -function benchmarkPattern(pattern: RegExp, worstCase: string): void { - const start = Date.now(); - pattern.test(worstCase); - if (Date.now() - start > 100) { - throw new Error('Pattern too slow on worst-case input'); - } -} -``` - -## Circular Reference Safety - -**Track visited nodes with a `WeakSet` when recursing into objects:** - -```typescript -// Bad: unbounded recursion -function recurse(obj: Record): void { - for (const key of Object.keys(obj)) { - recurse(obj[key] as Record); - } -} - -// Good: circular reference guard -function recurse(obj: Record, seen = new WeakSet()): void { - if (seen.has(obj)) return; - seen.add(obj); - for (const key of Object.keys(obj)) { - const val = obj[key]; - if (val && typeof val === 'object') { - recurse(val as Record, seen); - } - } -} -``` - -## Custom Pattern Validation - -**Treat user-provided pattern strings as untrusted. Validate before use:** - -```typescript -// Bad: user string passed directly to RegExp -const pattern = new RegExp(userInput); - -// Good: validate the pattern string first -function validatePatternString(input: string): void { - if (!/^[a-z][a-z0-9_]*$/i.test(input)) { - throw new DataSanitizationError(`Invalid pattern: ${input}`); - } -} -``` - -## Environment Variables and Secrets - -**Read secrets from environment variables. Never hardcode them:** - -```typescript -// Bad: hardcoded secret -const apiKey = 'sk-abc123'; - -// Good: read from environment -const apiKey = process.env.API_KEY; -if (!apiKey) throw new Error('Missing required env var: API_KEY'); -``` - -## Checklist - -- [ ] No original data values in error messages or details -- [ ] Inputs validated at library boundaries before processing -- [ ] Regex patterns tested for ReDoS on worst-case inputs -- [ ] Circular references handled with `WeakSet` guards -- [ ] User-provided pattern strings validated before `RegExp` construction -- [ ] No hardcoded tokens, keys, or secrets in source -- [ ] Dependencies audited for known vulnerabilities +#file:../../.ai/instructions/security.md diff --git a/.github/instructions/unit-tests.instructions.md b/.github/instructions/unit-tests.instructions.md index a0c01ad..6440673 100644 --- a/.github/instructions/unit-tests.instructions.md +++ b/.github/instructions/unit-tests.instructions.md @@ -2,155 +2,4 @@ applyTo: '**/*.test.js,**/*.test.ts,**/*.test.jsx,**/*.test.tsx' --- -# Unit Testing Standards (Vitest) - -**Applies to:** JavaScript/TypeScript tests. Other language test guidance is out of scope. - -## Core Principles - -1. Use Arrange, Act, and Assert comments for non-trivial tests; add Revert only - when the test performs cleanup -2. Aim for 100% coverage; use `/* istanbul ignore next */` with justification -3. BDD format: `describe()` blocks + `it()` (never use `test()`) -4. Test titles should start with `should` and describe behavior -5. Location: follow the repository's established test layout and naming patterns - -## Test Structure - -```javascript -import { describe, it, expect, beforeEach } from 'vitest'; -import { functionToTest } from './moduleToTest'; - -describe('moduleToTest.js', () => { - describe('functionToTest', () => { - it('should do something specific when given certain input', () => { - // Arrange - const input = 'test'; - - // Act - const result = functionToTest(input); - - // Assert - expect(result).toBe('expected'); - }); - }); -}); -``` - -## Coverage & Mocking - -**Coverage:** Use `/* istanbul ignore next */` only when necessary (error -boundaries, third-party code, platform-specific, E2E-tested UI). Add comment -explaining why. - -**Mocking:** Avoid when possible. If needed, mock at lowest level and document -why. Prefer refactoring for testability: - -```javascript -// Before: requires mocking Date -function getCurrentTime() { - return new Date().toISOString(); -} - -// After: testable via dependency injection -function getCurrentTime(date = new Date()) { - return date.toISOString(); -} -``` - -## Test Execution - -After any test file change (new tests, edits, refactors): - -1. Run the modified test file -2. Fix failures -3. Repeat until green - -Do not consider test changes complete until tests pass. - -## BDD Format Requirements - -- Use `describe()` for context and unit under test -- Use `it()` for test cases (never use `test()`) -- Titles should start with `should` + observable behavior -- One behavior per test when possible - -```javascript -describe('Calculator', () => { - describe('add', () => { - it('should return sum of two positive numbers', () => {}); - it('should handle negative numbers correctly', () => {}); - it('should throw error for non-numeric input', () => {}); - }); -}); -``` - -## AAA and Revert Comments - -Use in every non-trivial test: - -- `// Arrange` — setup/given -- `// Act` — when -- `// Assert` — then - -Use only when cleanup is actually performed: - -- `// Revert` — cleanup local test state, mocks, timers, files, or other side - effects not handled by `afterEach` - -Simple tests with obvious steps may omit these. - -## Example Test - -```javascript -import { describe, it, expect, beforeEach } from 'vitest'; -import { processRanges } from './labResults'; - -describe('labResults.js', () => { - describe('processRanges', () => { - let ranges, value, options; - - beforeEach(() => { - // Arrange - ranges = { - optimal: [[0, 10]], - warning: [[11, 20]], - danger: [[21, 30]], - }; - value = 15; - options = {}; - }); - - it('should return correct range info for value in warning range', () => { - // Act - const result = processRanges(ranges, value, options); - - // Assert - expect(result.activeRange.type).toBe('warning'); - expect(result.percent).toBeGreaterThan(0); - expect(result.percent).toBeLessThan(100); - }); - - it('should handle null ranges gracefully', () => { - // Arrange - ranges = null; - - // Act - const result = processRanges(ranges, value, options); - - // Assert - expect(result.noRanges).toBe(true); - expect(result.percent).toBe(50); - }); - }); -}); -``` - -## Checklist - -- [ ] Tests use `describe()` and `it()` -- [ ] Test titles start with `should` -- [ ] Arrange, Act, and Assert comments are present in non-trivial tests -- [ ] Revert comments are present only when the test performs cleanup -- [ ] Coverage exceptions include justification comments -- [ ] Modified tests are run and passing +#file:../../.ai/instructions/unit-tests.md diff --git a/.github/skills/plan-writing/SKILL.md b/.github/skills/plan-writing/SKILL.md index 1251217..8843478 100644 --- a/.github/skills/plan-writing/SKILL.md +++ b/.github/skills/plan-writing/SKILL.md @@ -7,39 +7,4 @@ description: > file conforming to the plan-writing standards. --- -# Plan Writing - -## When to Use - -- Starting any non-trivial implementation (feature, workflow, architectural change) -- When asked to "make a plan" or "write a plan" for a change -- Before creating a branch or issue for significant work - -## Format Rules - -Follow the instructions in -`.github/instructions/plan-writing.instructions.md` for required sections, -naming convention, and style. Do not restate those rules here. - -## Workflow - -1. **Determine the next plan number** — list `docs/plans/` and find the - highest existing `NNN` prefix; increment by one (pad to three digits) - -2. **Gather context** — read relevant source files, existing plans, and any - linked issues to understand scope before writing - -3. **Draft the plan** — create `docs/plans/NNN-description.md` using - `docs/plans/TEMPLATE.md` as the skeleton; fill every required section - -4. **Decisions first** — if the user has already discussed trade-offs in the - conversation, capture them in the Decisions section before they are lost - -5. **Confirm before saving** — present the draft to the user for review if the - scope is large or ambiguous; save directly for straightforward plans - -## Naming - -Use the filename description to summarize the change in two to four words, -lowercase kebab-case. The number is assigned at creation time and never -changes. +#file:../../../.ai/skills/plan-writing.md From 6be670bb9404cade01e2fd3e3b013e51993b6753 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Thu, 21 May 2026 00:10:11 -0400 Subject: [PATCH 05/10] feat: sync .ai/ layer with updated instruction files (jsdoc-tsdoc, remove performance, update security) Co-Authored-By: Claude Sonnet 4.6 --- .ai/instructions/jsdoc-tsdoc.md | 177 ++++++++++++ .ai/instructions/jsdoc.md | 132 --------- .ai/instructions/performance.md | 63 ---- .ai/instructions/security.md | 273 +++++++----------- .../instructions/jsdoc-tsdoc.instructions.md | 178 +----------- CLAUDE.md | 2 +- 6 files changed, 279 insertions(+), 546 deletions(-) create mode 100644 .ai/instructions/jsdoc-tsdoc.md delete mode 100644 .ai/instructions/jsdoc.md delete mode 100644 .ai/instructions/performance.md diff --git a/.ai/instructions/jsdoc-tsdoc.md b/.ai/instructions/jsdoc-tsdoc.md new file mode 100644 index 0000000..237ae01 --- /dev/null +++ b/.ai/instructions/jsdoc-tsdoc.md @@ -0,0 +1,177 @@ +# JSDoc & TSDoc Documentation Standards + +JavaScript files (`.js`, `.mjs`) use **JSDoc** with explicit `{Type}` +annotations because JavaScript has no type system. TypeScript files (`.ts`, +`.tsx`) use **TSDoc** — types already live in the signatures, so type +annotations are omitted from doc tags. + +## Core Principles (Both) + +1. **Every exported function must have a doc comment** — No exceptions. +2. **Include function description** — Clear explanation of what it does. +3. **Add `@example` for functions with I/O** — Show actual usage patterns. +4. **Add `@throws` for each error type** — Document every throw statement with + its specific error condition. + +--- + +## JSDoc — JavaScript Files (`.js`, `.mjs`) + +### Type Definition Rules + +**Use `import()` for external types:** + +```javascript +/** + * @typedef {import('node:fs').Stats} FileStats + * @typedef {import('./types.js').UserRecord} UserRecord + */ +``` + +**Define custom types as typedefs:** + +```javascript +/** + * @typedef {'low'|'medium'|'high'} Priority + * @typedef {Object} QueryFilters + * @property {string} [ownerId] - Owner identifier + * @property {number} [page=1] - Page number + */ +``` + +**Array types: use `Type[]`, not `Array`:** + +```javascript +@param {string[]} ids - User IDs +@returns {[number, number][]} Array of [min, max] pairs +``` + +### Complete Function Documentation + +```javascript +/** + * Retrieves records with optional pagination and filtering. + * + * @param {import('./repository.js').RecordRepository} repository - Data repository + * @param {QueryFilters} filters - Filter criteria + * @returns {Promise} Query results with metadata + * @throws {Error} When data retrieval fails + * + * @example + * const result = await getRecords(repository, { page: 1, limit: 50 }) + */ +async function getRecords(repository, filters) {} +``` + +### Common Patterns + +**Optional parameters with defaults:** + +```javascript +@param {number} [page=1] - Page number +@param {boolean} [includeCounts=false] - Include counts +``` + +**Destructured object parameters:** + +```javascript +/** + * @param {Object} options + * @param {string} options.name - User name + * @param {boolean} [options.active] - Whether the user is active + */ +function createUser({ name, active }) {} +``` + +**Multiple accepted types:** + +```javascript +@param {string|number} id - User ID +@returns {User|null} User object or null if not found +``` + +### Checklist + +- [ ] Every exported function has JSDoc +- [ ] All params documented with `{Type}` and description +- [ ] Return type documented with `{Type}` +- [ ] Optional params marked with `[param]` or `[param=default]` +- [ ] Array element types specified: `{string[]}` +- [ ] Async functions return `{Promise}` +- [ ] `@throws` present for every throw statement +- [ ] `@example` present for functions with parameters or return values + +--- + +## TSDoc — TypeScript Files (`.ts`, `.tsx`) + +Types live in the TypeScript signature. Do **not** include `{Type}` in `@param` +or `@returns` — TypeScript already provides that information. + +### Complete Function Documentation + +```typescript +/** + * Retrieves records with optional pagination and filtering. + * Adds computed metadata needed by downstream consumers. + * + * @param repository - Data repository instance. + * @param filters - Filter criteria for the query. + * @returns Query results with metadata attached. + * @throws {DataRetrievalError} When the data source is unavailable. + * + * @example + * const result = await getRecords(repository, { page: 1, limit: 50 }) + */ +async function getRecords( + repository: RecordRepository, + filters: QueryFilters, +): Promise {} +``` + +### Generic Type Parameters + +Document generic type parameters with `@typeParam`: + +```typescript +/** + * Wraps a value in an optional container. + * + * @typeParam T - The type of the wrapped value. + * @param value - Value to wrap. + * @returns The value wrapped in an optional container. + */ +function wrap(value: T): Optional {} +``` + +### Common Patterns + +**Optional parameters:** + +```typescript +/** + * @param data - Data to sanitize. + * @param options - Sanitization options. Uses defaults when omitted. + */ +function sanitize(data: unknown, options?: SanitizeOptions) {} +``` + +**Describe behavior, not type, in `@returns`:** + +```typescript +// Bad: just restates the return type +@returns {string} string + +// Good: describes what the value represents +@returns The sanitized string with all matched fields masked. +``` + +### Checklist + +- [ ] Every exported function has a TSDoc comment +- [ ] No `{Type}` annotations on `@param` or `@returns` +- [ ] All parameters documented with name and description +- [ ] `@returns` describes the value, not the type +- [ ] `@throws` present for every throw statement +- [ ] `@typeParam` used for generic type parameters +- [ ] `@example` present for functions with parameters or return values diff --git a/.ai/instructions/jsdoc.md b/.ai/instructions/jsdoc.md deleted file mode 100644 index 85ecd03..0000000 --- a/.ai/instructions/jsdoc.md +++ /dev/null @@ -1,132 +0,0 @@ -# JSDoc Documentation Standards - -## Core Principles - -1. **Every function must have JSDoc** — No exceptions. -2. **No inline type definitions** — Use typedefs or imported types. -3. **Import types from packages** — Use `import('package').Type` syntax -4. **Include function description** — Clear explanation of what it does -5. **Add @example for functions with I/O** — Show actual usage patterns -6. **Add @throws for each error type** — Document every throw statement with - specific error condition - -## Type Definition Rules - -**Use `import()` for external types:** - -```javascript -/** - * @typedef {import('node:fs').Stats} FileStats - * @typedef {import('node:http').IncomingHttpHeaders} IncomingHttpHeaders - * @typedef {import('./types.js').UserRecord} UserRecord - */ -``` - -**Define custom types as typedefs:** - -```javascript -/** - * @typedef {'low'|'medium'|'high'} Priority - * @typedef {Object} QueryFilters - * @property {string} [ownerId] - Owner identifier - * @property {string} [status] - Optional status filter - * @property {number} [page=1] - Page number - * @property {number} [limit=100] - Items per page - */ -``` - -**Use `[...]` for arrays, not `Array<...>`:** - -```javascript -@param {[number, number][]} ranges - Array of [min, max] pairs -@returns {string[]} Array of user IDs -``` - -## Complete Function Documentation - -Every function should include: - -1. **Description** — What the function does -2. **@param** — All parameters with imported/typedef types -3. **@returns** — Return type (use `Promise` for async) -4. **@throws** — Document error conditions -5. **@example** — Usage example for functions with parameters or return values - -```javascript -/** - * Retrieves records with optional pagination and filtering. - * Adds computed metadata needed by downstream consumers. - * - * @param {import('./repository.js').RecordRepository} repository - Data repository - * @param {QueryFilters} filters - Filter criteria - * @param {import('./logger.js').Logger} logger - Logger instance - * @returns {Promise} Query results with metadata - * @throws {Error} When data retrieval fails - * @throws {Error} When response shaping fails - * - * @example - * // Get records with pagination - * const result = await getRecords(repository, { - * ownerId: 'user-123', - * page: 1, - * limit: 50 - * }, logger) - * - * @example - * // Get records within a date range - * const result = await getRecords(repository, { - * ownerId: 'user-456', - * start_date: '2025-01-01T00:00:00Z', - * end_date: '2025-01-31T23:59:59Z' - * }, logger) - */ -async function getRecords(repository, filters, logger) { - // Implementation -} -``` - -## Common Patterns - -**Optional parameters with defaults:** - -```javascript -@param {number} [page=1] - Page number -@param {boolean} [include_counts=false] - Include counts -``` - -**Multiple type options:** - -```javascript -@param {string|number} id - User ID -@returns {User|null} User object or null if not found -``` - -**Destructured parameters:** - -```javascript -/** - * @param {Object} options - * @param {string} options.ownerId - Owner identifier - * @param {boolean} [options.includeCounts] - Include counts - */ -function process({ ownerId, includeCounts }) {} -``` - -**Callback/function types:** - -```javascript -@param {(error: Error|null, result: any) => void} callback -``` - -## Checklist - -When documenting types, verify: - -- [ ] Types match actual data passed to function -- [ ] Return types match actual returned data -- [ ] Optional params marked with `[param]` or `[param=default]` -- [ ] Nullable types marked with `|null` or `|undefined` -- [ ] Union types used for multiple accepted types -- [ ] No inline `Object` — use typedef or imported type -- [ ] Array element types specified: `[ElementType]` -- [ ] Async functions return `Promise` diff --git a/.ai/instructions/performance.md b/.ai/instructions/performance.md deleted file mode 100644 index 5a9afda..0000000 --- a/.ai/instructions/performance.md +++ /dev/null @@ -1,63 +0,0 @@ -# Performance Guidelines - -## Core Principles - -1. **Readability first** — Optimize only when needed -2. **Profile before optimizing** — Measure, don't guess -3. **Document performance-critical code** — Explain why optimizations exist -4. **Split large files** — Break into focused modules when exceeding 300 lines - -## Memory Management - -**Always clean up resources:** - -```javascript -// Good: Cleanup event listeners -function startWatcher(target) { - const onChange = () => { - /* ... */ - }; - target.addEventListener('change', onChange); - return () => target.removeEventListener('change', onChange); -} - -// Good: Cleanup timers -const timer = setInterval(runWork, 1000); -clearInterval(timer); -``` - -## Compute Optimization - -**Cache expensive calculations when inputs are stable:** - -```javascript -const cache = new Map(); - -function computeDigest(input) { - if (cache.has(input)) return cache.get(input); - const result = expensiveOperation(input); - cache.set(input, result); - return result; -} -``` - -## Code Splitting - -**Lazy load heavy modules:** - -```javascript -async function loadAnalyzer() { - const { analyze } = await import('./analyzer.js'); - return analyze; -} -``` - -## Checklist - -- [ ] Event listeners and timers cleaned up -- [ ] Large objects released when no longer needed -- [ ] Expensive calculations cached where beneficial -- [ ] Heavy modules lazy loaded where practical -- [ ] Files split when exceeding 300 lines -- [ ] Code profiled before optimization -- [ ] Performance-critical code documented diff --git a/.ai/instructions/security.md b/.ai/instructions/security.md index 621293d..5986eeb 100644 --- a/.ai/instructions/security.md +++ b/.ai/instructions/security.md @@ -2,220 +2,147 @@ ## Core Principles -1. **Validate at the edge** — Validate request shape and value constraints on all external inputs. -2. **Sanitize untrusted content** — Sanitize user-provided markup and normalize text input. -3. **Verify signatures** — Verify signatures for all inbound webhooks or callbacks. -4. **Use safe identifier conversion** — Parse and validate untrusted IDs with strict checks and try/catch. -5. **Fail securely** — Log internal details server-side and return generic client errors. -6. **Least privilege** — Restrict access with explicit authorization policies. -7. **Secrets in environment only** — Validate required secrets at startup and never hardcode credentials. - -## Input Validation - -**Every endpoint should validate request body, params, and query values:** - -```javascript -// Good: Validate request before business logic -const schema = { - type: 'object', - required: ['email', 'password'], - properties: { - email: { type: 'string', format: 'email' }, - password: { type: 'string', minLength: 8 }, - }, -}; - -function createUserHandler(request, response) { - const { valid, errors } = validate(schema, request.body); - if (!valid) { - return response.status(400).json({ error: 'Invalid request payload' }); - } - - const { email, password } = request.body; - return response.status(201).json({ email }); -} - -// Bad: No validation -function createUserHandlerUnsafe(request, response) { - const { email, password } = request.body; // Unsafe - return response.status(201).json({ email, password }); -} +1. **Never expose data in errors** — Error messages must not contain the + original input payload. Use type labels or error categories only. +2. **Validate inputs at library boundaries** — Check that inputs match expected + types before processing. Fail fast with a typed error. +3. **Guard against ReDoS** — Regex patterns that process untrusted input must + avoid catastrophic backtracking. Test patterns against worst-case inputs. +4. **Handle circular references safely** — When recursing into objects, track + visited nodes to prevent infinite loops and stack overflows. +5. **Validate custom patterns** — User-provided pattern strings are untrusted + input. Validate or escape before passing to `RegExp`. +6. **No hardcoded credentials** — Never commit tokens, keys, or secrets to + source. Read secrets from environment variables. +7. **Minimal dependency surface** — Keep dependencies minimal. Audit for known + vulnerabilities regularly. + +## Data Safety in Errors + +**Never include the original value in thrown errors or error details:** + +```typescript +// Bad: exposes the payload +throw new Error(`Cannot sanitize value: ${JSON.stringify(data)}`); + +// Good: only exposes the type +throw new DataSanitizationError( + `Cannot sanitize value of type: ${getInputType(data)}`, +); ``` -## Security Headers - -**Set defensive security headers for all HTTP responses:** +**Use type labels, not values, in all diagnostic output:** -```javascript -function applySecurityHeaders(response) { - response.setHeader('X-Content-Type-Options', 'nosniff'); - response.setHeader('X-Frame-Options', 'DENY'); - response.setHeader('Referrer-Policy', 'no-referrer'); - response.setHeader('Content-Security-Policy', "default-src 'self'"); +```typescript +function getInputType(data: unknown): string { + if (data === null) return 'null'; + if (Array.isArray(data)) return 'array'; + return typeof data; } ``` -## Safe Identifier Handling +## Input Validation at Library Boundaries -**Always validate identifier format before database access:** - -```javascript -function parseStrictId(id) { - if (!/^[a-f0-9]{24}$/i.test(id)) { - throw new Error('Invalid ID format'); - } - return id.toLowerCase(); -} +**Check types before processing and throw typed errors early:** -function findById(id, repository) { - try { - const safeId = parseStrictId(id); - return repository.findOne({ id: safeId }); - } catch { - return null; +```typescript +// Good: validate before recursing +function sanitizeObject(data: unknown): unknown { + if (data === null || typeof data !== 'object') { + throw new DataSanitizationError( + `Expected object, got ${getInputType(data)}`, + ); } + return processObject(data); } ``` -## Webhook Signature Verification - -**Always verify webhook signatures before processing payloads:** - -```javascript -import crypto from 'node:crypto'; - -function verifyWebhookSignature({ payload, signature, secret }) { - const expected = crypto - .createHmac('sha256', secret) - .update(payload) - .digest('hex'); - return crypto.timingSafeEqual( - Buffer.from(expected), - Buffer.from(signature || ''), - ); -} +## ReDoS Protection -function handleWebhook(request, response, secret) { - const signature = request.headers['x-signature']; - const valid = verifyWebhookSignature({ - payload: request.rawBody, - signature, - secret, - }); +**Avoid patterns with nested quantifiers or overlapping alternations on +untrusted input:** - if (!valid) { - return response.status(401).json({ error: 'Invalid signature' }); - } +```typescript +// Risky: nested quantifiers can cause catastrophic backtracking +const unsafe = /^(a+)+$/; - return response.status(200).json({ ok: true }); -} +// Safe: linear patterns with clear boundaries +const safe = /^a+$/; ``` -## Authentication and Authorization - -**Protect private routes and authorize by capability/role:** +**Test custom patterns against long repeated inputs before using them:** -```javascript -function requireAuth(request) { - if (!request.user) { - throw new Error('Unauthorized'); +```typescript +// Validate performance before accepting a user pattern +function benchmarkPattern(pattern: RegExp, worstCase: string): void { + const start = Date.now(); + pattern.test(worstCase); + if (Date.now() - start > 100) { + throw new Error('Pattern too slow on worst-case input'); } } - -function requirePermission(user, permission) { - if (!user.permissions.includes(permission)) { - throw new Error('Forbidden'); - } -} - -function createAdminUser(request, response) { - requireAuth(request); - requirePermission(request.user, 'users:create'); - return response.status(201).json({ ok: true }); -} ``` -## Data Protection - -### Rate Limiting +## Circular Reference Safety -```javascript -function createRateLimiter({ max, windowMs }) { - const hits = new Map(); +**Track visited nodes with a `WeakSet` when recursing into objects:** - return function allow(ip, now = Date.now()) { - const slot = hits.get(ip) || []; - const recent = slot.filter((ts) => now - ts < windowMs); - if (recent.length >= max) return false; - recent.push(now); - hits.set(ip, recent); - return true; - }; +```typescript +// Bad: unbounded recursion +function recurse(obj: Record): void { + for (const key of Object.keys(obj)) { + recurse(obj[key] as Record); + } } -``` -### Input Sanitization - -**Sanitize untrusted user content before persistence or rendering:** - -```javascript -function sanitizeInput(value) { - if (typeof value === 'string') { - return value.replace(/[<>]/g, ''); - } - if (Array.isArray(value)) { - return value.map(sanitizeInput); - } - if (value && typeof value === 'object') { - const next = {}; - for (const [key, item] of Object.entries(value)) { - next[key] = sanitizeInput(item); +// Good: circular reference guard +function recurse(obj: Record, seen = new WeakSet()): void { + if (seen.has(obj)) return; + seen.add(obj); + for (const key of Object.keys(obj)) { + const val = obj[key]; + if (val && typeof val === 'object') { + recurse(val as Record, seen); } - return next; } - return value; } ``` -### Environment Variables and Secrets +## Custom Pattern Validation -**Define and validate all required environment variables at startup:** +**Treat user-provided pattern strings as untrusted. Validate before use:** -```javascript -const requiredEnv = ['JWT_SIGNING_KEY', 'WEBHOOK_SIGNING_SECRET']; +```typescript +// Bad: user string passed directly to RegExp +const pattern = new RegExp(userInput); -function validateEnvironment(env = process.env) { - for (const key of requiredEnv) { - if (!env[key] || env[key].trim() === '') { - throw new Error(`Missing required environment variable: ${key}`); - } +// Good: validate the pattern string first +function validatePatternString(input: string): void { + if (!/^[a-z][a-z0-9_]*$/i.test(input)) { + throw new DataSanitizationError(`Invalid pattern: ${input}`); } } ``` -## Error Handling +## Environment Variables and Secrets -**Never expose internal details to external clients:** +**Read secrets from environment variables. Never hardcode them:** -```javascript -try { - await riskyOperation(); -} catch (error) { - logger.error({ error }, 'Operation failed'); - return response.status(500).json({ error: 'Internal server error' }); -} +```typescript +// Bad: hardcoded secret +const apiKey = 'sk-abc123'; + +// Good: read from environment +const apiKey = process.env.API_KEY; +if (!apiKey) throw new Error('Missing required env var: API_KEY'); ``` ## Checklist -- [ ] Input validation on all external inputs -- [ ] Authentication and authorization on protected routes -- [ ] Rate limiting on externally accessible APIs -- [ ] Security headers configured -- [ ] Error responses avoid internal details -- [ ] Injection/XSS mitigations in place -- [ ] Webhook signatures verified before processing -- [ ] ID parsing and conversion wrapped in strict validation -- [ ] Untrusted input sanitized before storage/rendering -- [ ] Secrets loaded only from environment variables -- [ ] No sensitive data in logs +- [ ] No original data values in error messages or details +- [ ] Inputs validated at library boundaries before processing +- [ ] Regex patterns tested for ReDoS on worst-case inputs +- [ ] Circular references handled with `WeakSet` guards +- [ ] User-provided pattern strings validated before `RegExp` construction +- [ ] No hardcoded tokens, keys, or secrets in source +- [ ] Dependencies audited for known vulnerabilities diff --git a/.github/instructions/jsdoc-tsdoc.instructions.md b/.github/instructions/jsdoc-tsdoc.instructions.md index f2b1643..e742aed 100644 --- a/.github/instructions/jsdoc-tsdoc.instructions.md +++ b/.github/instructions/jsdoc-tsdoc.instructions.md @@ -2,180 +2,4 @@ applyTo: '**/*.js,**/*.mjs,**/*.ts,**/*.tsx' --- -# JSDoc & TSDoc Documentation Standards - -JavaScript files (`.js`, `.mjs`) use **JSDoc** with explicit `{Type}` -annotations because JavaScript has no type system. TypeScript files (`.ts`, -`.tsx`) use **TSDoc** — types already live in the signatures, so type -annotations are omitted from doc tags. - -## Core Principles (Both) - -1. **Every exported function must have a doc comment** — No exceptions. -2. **Include function description** — Clear explanation of what it does. -3. **Add `@example` for functions with I/O** — Show actual usage patterns. -4. **Add `@throws` for each error type** — Document every throw statement with - its specific error condition. - ---- - -## JSDoc — JavaScript Files (`.js`, `.mjs`) - -### Type Definition Rules - -**Use `import()` for external types:** - -```javascript -/** - * @typedef {import('node:fs').Stats} FileStats - * @typedef {import('./types.js').UserRecord} UserRecord - */ -``` - -**Define custom types as typedefs:** - -```javascript -/** - * @typedef {'low'|'medium'|'high'} Priority - * @typedef {Object} QueryFilters - * @property {string} [ownerId] - Owner identifier - * @property {number} [page=1] - Page number - */ -``` - -**Array types: use `Type[]`, not `Array`:** - -```javascript -@param {string[]} ids - User IDs -@returns {[number, number][]} Array of [min, max] pairs -``` - -### Complete Function Documentation - -```javascript -/** - * Retrieves records with optional pagination and filtering. - * - * @param {import('./repository.js').RecordRepository} repository - Data repository - * @param {QueryFilters} filters - Filter criteria - * @returns {Promise} Query results with metadata - * @throws {Error} When data retrieval fails - * - * @example - * const result = await getRecords(repository, { page: 1, limit: 50 }) - */ -async function getRecords(repository, filters) {} -``` - -### Common Patterns - -**Optional parameters with defaults:** - -```javascript -@param {number} [page=1] - Page number -@param {boolean} [includeCounts=false] - Include counts -``` - -**Destructured object parameters:** - -```javascript -/** - * @param {Object} options - * @param {string} options.name - User name - * @param {boolean} [options.active] - Whether the user is active - */ -function createUser({ name, active }) {} -``` - -**Multiple accepted types:** - -```javascript -@param {string|number} id - User ID -@returns {User|null} User object or null if not found -``` - -### Checklist - -- [ ] Every exported function has JSDoc -- [ ] All params documented with `{Type}` and description -- [ ] Return type documented with `{Type}` -- [ ] Optional params marked with `[param]` or `[param=default]` -- [ ] Array element types specified: `{string[]}` -- [ ] Async functions return `{Promise}` -- [ ] `@throws` present for every throw statement -- [ ] `@example` present for functions with parameters or return values - ---- - -## TSDoc — TypeScript Files (`.ts`, `.tsx`) - -Types live in the TypeScript signature. Do **not** include `{Type}` in `@param` -or `@returns` — TypeScript already provides that information. - -### Complete Function Documentation - -```typescript -/** - * Retrieves records with optional pagination and filtering. - * Adds computed metadata needed by downstream consumers. - * - * @param repository - Data repository instance. - * @param filters - Filter criteria for the query. - * @returns Query results with metadata attached. - * @throws {DataRetrievalError} When the data source is unavailable. - * - * @example - * const result = await getRecords(repository, { page: 1, limit: 50 }) - */ -async function getRecords( - repository: RecordRepository, - filters: QueryFilters, -): Promise {} -``` - -### Generic Type Parameters - -Document generic type parameters with `@typeParam`: - -```typescript -/** - * Wraps a value in an optional container. - * - * @typeParam T - The type of the wrapped value. - * @param value - Value to wrap. - * @returns The value wrapped in an optional container. - */ -function wrap(value: T): Optional {} -``` - -### Common Patterns - -**Optional parameters:** - -```typescript -/** - * @param data - Data to sanitize. - * @param options - Sanitization options. Uses defaults when omitted. - */ -function sanitize(data: unknown, options?: SanitizeOptions) {} -``` - -**Describe behavior, not type, in `@returns`:** - -```typescript -// Bad: just restates the return type -@returns {string} string - -// Good: describes what the value represents -@returns The sanitized string with all matched fields masked. -``` - -### Checklist - -- [ ] Every exported function has a TSDoc comment -- [ ] No `{Type}` annotations on `@param` or `@returns` -- [ ] All parameters documented with name and description -- [ ] `@returns` describes the value, not the type -- [ ] `@throws` present for every throw statement -- [ ] `@typeParam` used for generic type parameters -- [ ] `@example` present for functions with parameters or return values +#file:../../.ai/instructions/jsdoc-tsdoc.md diff --git a/CLAUDE.md b/CLAUDE.md index 600297e..b7d78a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,5 +30,5 @@ This is a TypeScript library that sanitizes sensitive data in objects and string @.ai/instructions/code-complexity.md @.ai/instructions/comments.md -@.ai/instructions/jsdoc.md +@.ai/instructions/jsdoc-tsdoc.md @.ai/instructions/unit-tests.md From cedd9807fffab61b8fa1ae3fba8a354df3635783 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Thu, 21 May 2026 00:11:33 -0400 Subject: [PATCH 06/10] feat: add ai-add-instruction/prompt/skill maintenance skills Co-Authored-By: Claude Sonnet 4.6 --- .ai/skills/ai-add-instruction.md | 60 ++++++++++++++++++++++++++++++++ .ai/skills/ai-add-prompt.md | 53 ++++++++++++++++++++++++++++ .ai/skills/ai-add-skill.md | 56 +++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 .ai/skills/ai-add-instruction.md create mode 100644 .ai/skills/ai-add-prompt.md create mode 100644 .ai/skills/ai-add-skill.md diff --git a/.ai/skills/ai-add-instruction.md b/.ai/skills/ai-add-instruction.md new file mode 100644 index 0000000..ec2905a --- /dev/null +++ b/.ai/skills/ai-add-instruction.md @@ -0,0 +1,60 @@ +# ai-add-instruction + +Creates a new shared instruction and registers thin wrappers for each AI system +detected in the repo. + +## When to Use + +When adding a new auto-loaded coding standard or guideline that should apply to +both Copilot and Claude Code. + +## AI System Detection + +Before creating any files, check: + +- **Copilot instructions active**: `.github/instructions/` directory exists +- **Claude active**: `CLAUDE.md` file exists + +## Steps + +1. **Gather inputs** — Ask for: + - `name`: kebab-case identifier (e.g. `naming-conventions`) + - `applyTo`: glob pattern for Copilot's `applyTo` frontmatter (e.g. `**` or + `**/*.ts,**/*.tsx`) + - Brief description of the instruction's purpose + +2. **Create the shared file** — Create `.ai/instructions/.md` with this + starter template: + + ```markdown + # + + <!-- Brief description of what this instruction covers --> + + ## Core Principles + + 1. + + ## Checklist + + - [ ] + ``` + +3. **If Copilot instructions active** — Create + `.github/instructions/<name>.instructions.md`: + + ```markdown + --- + applyTo: '<applyTo value>' + --- + + #file:../../.ai/instructions/<name>.md + ``` + +4. **If Claude active** — Append to `CLAUDE.md`'s `## Conventions` section: + + ``` + @.ai/instructions/<name>.md + ``` + +5. **Confirm** — Report which files were created. diff --git a/.ai/skills/ai-add-prompt.md b/.ai/skills/ai-add-prompt.md new file mode 100644 index 0000000..d1f20b5 --- /dev/null +++ b/.ai/skills/ai-add-prompt.md @@ -0,0 +1,53 @@ +# ai-add-prompt + +Creates a new shared prompt and registers thin wrappers for each AI system +detected in the repo. + +## When to Use + +When adding a new user-invokable slash command / chat prompt that should be +available in both Copilot and Claude Code. + +## AI System Detection + +Before creating any files, check: + +- **Copilot prompts active**: `.github/prompts/` directory exists +- **Claude active**: `CLAUDE.md` file exists (create in `.claude/skills/`) + +## Steps + +1. **Gather inputs** — Ask for: + - `name`: kebab-case identifier (e.g. `create-component`) + - `description`: one-sentence summary shown in the prompt picker + +2. **Create the shared file** — Create `.ai/prompts/<name>.md` (create + `.ai/prompts/` directory if it does not exist) with this starter template: + + ```markdown + # <Title> + + <!-- Description: <description> --> + + ## Steps + + 1. + ``` + +3. **If Copilot prompts active** — Create `.github/prompts/<name>.prompt.md`: + + ```markdown + --- + description: '<description>' + --- + + #file:../../.ai/prompts/<name>.md + ``` + +4. **If Claude active** — Create `.claude/skills/<name>.md`: + + ``` + @.ai/prompts/<name>.md + ``` + +5. **Confirm** — Report which files were created. diff --git a/.ai/skills/ai-add-skill.md b/.ai/skills/ai-add-skill.md new file mode 100644 index 0000000..8e8c886 --- /dev/null +++ b/.ai/skills/ai-add-skill.md @@ -0,0 +1,56 @@ +# ai-add-skill + +Creates a new shared skill and registers thin wrappers for each AI system +detected in the repo. + +## When to Use + +When adding a new reusable workflow that should be invokable by name in both +Copilot and Claude Code. + +## AI System Detection + +Before creating any files, check: + +- **Copilot skills active**: `.github/skills/` directory exists +- **Claude active**: `CLAUDE.md` file exists (create in `.claude/skills/`) + +## Steps + +1. **Gather inputs** — Ask for: + - `name`: kebab-case identifier (e.g. `run-tests`) + - `description`: one-sentence summary shown in the skill picker + +2. **Create the shared file** — Create `.ai/skills/<name>.md` with this starter + template: + + ```markdown + # <Title> + + <!-- Description: <description> --> + + ## When to Use + + ## Steps + + 1. + ``` + +3. **If Copilot skills active** — Create `.github/skills/<name>/SKILL.md`: + + ```markdown + --- + name: <name> + description: '<description>' + --- + + #file:../../../.ai/skills/<name>.md + ``` + +4. **If Claude active** — Create `.claude/skills/<name>.md`: + + ``` + @.ai/skills/<name>.md + ``` + +5. **Confirm** — Report which files were created. From f7fb514e807e2d4752a236d5d8c5d153fd1955fa Mon Sep 17 00:00:00 2001 From: Mark Jubenville <ioncache@gmail.com> Date: Thu, 21 May 2026 00:13:16 -0400 Subject: [PATCH 07/10] feat: add thin wrappers for ai-add-* maintenance skills --- .claude/skills/ai-add-instruction.md | 1 + .claude/skills/ai-add-prompt.md | 1 + .claude/skills/ai-add-skill.md | 1 + .github/skills/ai-add-instruction/SKILL.md | 8 ++++++++ .github/skills/ai-add-prompt/SKILL.md | 8 ++++++++ .github/skills/ai-add-skill/SKILL.md | 8 ++++++++ 6 files changed, 27 insertions(+) create mode 100644 .claude/skills/ai-add-instruction.md create mode 100644 .claude/skills/ai-add-prompt.md create mode 100644 .claude/skills/ai-add-skill.md create mode 100644 .github/skills/ai-add-instruction/SKILL.md create mode 100644 .github/skills/ai-add-prompt/SKILL.md create mode 100644 .github/skills/ai-add-skill/SKILL.md diff --git a/.claude/skills/ai-add-instruction.md b/.claude/skills/ai-add-instruction.md new file mode 100644 index 0000000..0f99d90 --- /dev/null +++ b/.claude/skills/ai-add-instruction.md @@ -0,0 +1 @@ +@.ai/skills/ai-add-instruction.md diff --git a/.claude/skills/ai-add-prompt.md b/.claude/skills/ai-add-prompt.md new file mode 100644 index 0000000..0948ecb --- /dev/null +++ b/.claude/skills/ai-add-prompt.md @@ -0,0 +1 @@ +@.ai/skills/ai-add-prompt.md diff --git a/.claude/skills/ai-add-skill.md b/.claude/skills/ai-add-skill.md new file mode 100644 index 0000000..cc28354 --- /dev/null +++ b/.claude/skills/ai-add-skill.md @@ -0,0 +1 @@ +@.ai/skills/ai-add-skill.md diff --git a/.github/skills/ai-add-instruction/SKILL.md b/.github/skills/ai-add-instruction/SKILL.md new file mode 100644 index 0000000..7b2d39a --- /dev/null +++ b/.github/skills/ai-add-instruction/SKILL.md @@ -0,0 +1,8 @@ +--- +name: ai-add-instruction +description: > + Creates a new shared instruction in .ai/instructions/ and registers thin + wrappers for all detected AI systems (Copilot, Claude Code). +--- + +#file:../../../.ai/skills/ai-add-instruction.md diff --git a/.github/skills/ai-add-prompt/SKILL.md b/.github/skills/ai-add-prompt/SKILL.md new file mode 100644 index 0000000..ece6188 --- /dev/null +++ b/.github/skills/ai-add-prompt/SKILL.md @@ -0,0 +1,8 @@ +--- +name: ai-add-prompt +description: > + Creates a new shared prompt in .ai/prompts/ and registers thin wrappers for + all detected AI systems (Copilot, Claude Code). +--- + +#file:../../../.ai/skills/ai-add-prompt.md diff --git a/.github/skills/ai-add-skill/SKILL.md b/.github/skills/ai-add-skill/SKILL.md new file mode 100644 index 0000000..d7d5e0e --- /dev/null +++ b/.github/skills/ai-add-skill/SKILL.md @@ -0,0 +1,8 @@ +--- +name: ai-add-skill +description: > + Creates a new shared skill in .ai/skills/ and registers thin wrappers for all + detected AI systems (Copilot, Claude Code). +--- + +#file:../../../.ai/skills/ai-add-skill.md From 476c7b4e68c313de73a9e4fa9651774c9764fe40 Mon Sep 17 00:00:00 2001 From: Mark Jubenville <ioncache@gmail.com> Date: Thu, 21 May 2026 00:13:47 -0400 Subject: [PATCH 08/10] docs: add plan 008 for ai customization shared layer --- .../008-ai-customization-shared-layer.md | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 docs/plans/008-ai-customization-shared-layer.md diff --git a/docs/plans/008-ai-customization-shared-layer.md b/docs/plans/008-ai-customization-shared-layer.md new file mode 100644 index 0000000..131999d --- /dev/null +++ b/docs/plans/008-ai-customization-shared-layer.md @@ -0,0 +1,270 @@ +# AI Customization Shared Layer + +## Approach + +Create a `.ai/` directory as a single source of truth for all AI customization +content — instructions, skills, and prompts. Both GitHub Copilot and Claude Code +currently maintain separate copies of the same rules (instruction files for +Copilot, inline text in CLAUDE.md for Claude). This plan migrates all shared +content into `.ai/`, strips the bodies from the existing Copilot files so they +become thin wrappers, and creates equivalent thin wrappers for Claude Code. A +set of maintenance skills (`ai-add-instruction`, `ai-add-prompt`, `ai-add-skill`) +ensures new content is always created in the shared layer with wrappers +auto-generated for every detected AI system. + +## Nomenclature Mapping + +The systems use different names for the same concepts: + +| Concept | Copilot location | Claude Code location | `.ai/` subfolder | +| ---------------------- | -------------------------------- | -------------------- | ---------------- | +| Auto-loaded context | `.github/instructions/` | CLAUDE.md `@imports` | `instructions/` | +| User-invokable prompts | `.github/prompts/` | `.claude/skills/` | `prompts/` | +| Reusable workflows | `.github/skills/<name>/SKILL.md` | `.claude/skills/` | `skills/` | + +## Thin Wrapper Formats + +**Copilot instruction** — keep frontmatter, replace body: + +```markdown +--- +applyTo: '**' +--- + +#file:../../.ai/instructions/<name>.md +``` + +**Copilot skill** — keep frontmatter, replace body: + +```markdown +--- +name: <name> +description: '...' +--- + +#file:../../../.ai/skills/<name>.md +``` + +**Copilot prompt** — keep frontmatter, replace body: + +```markdown +--- +description: '...' +--- + +#file:../../.ai/prompts/<name>.md +``` + +**Claude skill** (`.claude/skills/<name>.md`) — no frontmatter: + +``` +@.ai/skills/<name>.md +``` + +**Claude prompt** (`.claude/skills/<name>.md`) — no frontmatter: + +``` +@.ai/prompts/<name>.md +``` + +**CLAUDE.md Conventions section** — replace inline bullets with `@imports`: + +```markdown +## Conventions + +@.ai/instructions/code-complexity.md +@.ai/instructions/comments.md +@.ai/instructions/jsdoc.md +@.ai/instructions/unit-tests.md +``` + +Note: `code-review` is excluded from CLAUDE.md (its header states it is for +Copilot review only, not coding assistants). `plan-writing` is excluded because +`docs/development.md` already covers the plan workflow for Claude. + +**Cross-references within `.ai/` files** — when a shared file must reference +another shared file, include both syntaxes: + +```markdown +Follow the standards in `.ai/instructions/plan-writing.md`. + +> **Claude Code:** `@.ai/instructions/plan-writing.md` +> **GitHub Copilot:** `#file:../../.ai/instructions/plan-writing.md` +``` + +## Steps + +### Phase 1 — Create the shared layer + +1. Create `.ai/instructions/` and `.ai/skills/` directories. + +2. For each `.github/instructions/*.instructions.md` file, create a + corresponding `.ai/instructions/<name>.md` by copying the body (everything + after the closing `---` of the frontmatter). Files to migrate: + - `code-complexity.instructions.md` → `.ai/instructions/code-complexity.md` + - `code-review.instructions.md` → `.ai/instructions/code-review.md` + - `comments.instructions.md` → `.ai/instructions/comments.md` + - `jsdoc.instructions.md` (or `jsdoc-tsdoc.instructions.md` if that file + has replaced it) → `.ai/instructions/jsdoc.md` + - `plan-writing.instructions.md` → `.ai/instructions/plan-writing.md` + - `security.instructions.md` → `.ai/instructions/security.md` + - `unit-tests.instructions.md` → `.ai/instructions/unit-tests.md` + +3. Create `.ai/skills/plan-writing.md` from the body of + `.github/skills/plan-writing/SKILL.md`. Update the internal reference from + `.github/instructions/plan-writing.instructions.md` to use the cross-reference + pattern (both Claude and Copilot syntax pointing to + `.ai/instructions/plan-writing.md`). + +4. Create `.ai/README.md` explaining the folder structure and noting that + `ai-add-*` skills are candidates for future extraction into a Claude plugin + or Copilot user-level skills (`~/.copilot/skills/`). + +### Phase 2 — Update Copilot thin wrappers + +5. For each `.github/instructions/*.instructions.md`, replace the body with a + `#file:` reference to the corresponding `.ai/instructions/<name>.md`. Keep + all frontmatter (`applyTo`, `name`, `description`) unchanged. + +6. Replace the body of `.github/skills/plan-writing/SKILL.md` with a `#file:` + reference to `.ai/skills/plan-writing.md`. Keep frontmatter unchanged. + +### Phase 3 — Create Claude thin wrappers + +7. Create `.claude/skills/` directory. + +8. Create `.claude/skills/plan-writing.md` containing only: + + ``` + @.ai/skills/plan-writing.md + ``` + +9. Update the `## Conventions` section of `CLAUDE.md` to replace the three + inline bullet points with `@import` lines for `code-complexity.md`, + `comments.md`, `jsdoc.md`, and `unit-tests.md`. + +### Phase 4 — Maintenance skills + +10. Create `.ai/skills/ai-add-instruction.md`. This skill: + - Asks for a name (kebab-case) and `applyTo` glob + - Creates `.ai/instructions/<name>.md` with a starter template + - Detects Copilot (`.github/instructions/` exists) and creates + `.github/instructions/<name>.instructions.md` thin wrapper + - Detects Claude (`CLAUDE.md` exists) and appends `@.ai/instructions/<name>.md` + to the Conventions section of `CLAUDE.md` + +11. Create `.ai/skills/ai-add-prompt.md`. This skill: + - Asks for a name and description + - Creates `.ai/prompts/<name>.md` (creates `.ai/prompts/` if absent) + - Detects Copilot and creates `.github/prompts/<name>.prompt.md` thin wrapper + - Detects Claude and creates `.claude/skills/<name>.md` thin wrapper + +12. Create `.ai/skills/ai-add-skill.md`. This skill: + - Asks for a name and description + - Creates `.ai/skills/<name>.md` with a starter template + - Detects Copilot and creates `.github/skills/<name>/SKILL.md` thin wrapper + - Detects Claude and creates `.claude/skills/<name>.md` thin wrapper + +13. Create Copilot thin wrappers for the three maintenance skills: + - `.github/skills/ai-add-instruction/SKILL.md` + - `.github/skills/ai-add-prompt/SKILL.md` + - `.github/skills/ai-add-skill/SKILL.md` + +14. Create Claude thin wrappers for the three maintenance skills: + - `.claude/skills/ai-add-instruction.md` + - `.claude/skills/ai-add-prompt.md` + - `.claude/skills/ai-add-skill.md` + +## Relevant Files + +**New — shared layer:** + +- `.ai/README.md` — explains the folder structure +- `.ai/instructions/code-complexity.md` — new (content from Copilot wrapper) +- `.ai/instructions/code-review.md` — new +- `.ai/instructions/comments.md` — new +- `.ai/instructions/jsdoc.md` — new +- `.ai/instructions/plan-writing.md` — new +- `.ai/instructions/security.md` — new +- `.ai/instructions/unit-tests.md` — new +- `.ai/skills/plan-writing.md` — new (content from Copilot wrapper) +- `.ai/skills/ai-add-instruction.md` — new maintenance skill +- `.ai/skills/ai-add-prompt.md` — new maintenance skill +- `.ai/skills/ai-add-skill.md` — new maintenance skill + +**Updated — Copilot thin wrappers:** + +- `.github/instructions/code-complexity.instructions.md` — updated (body replaced) +- `.github/instructions/code-review.instructions.md` — updated (body replaced) +- `.github/instructions/comments.instructions.md` — updated (body replaced) +- `.github/instructions/jsdoc.instructions.md` (or `jsdoc-tsdoc.instructions.md`) — updated (body replaced) +- `.github/instructions/plan-writing.instructions.md` — updated (body replaced) +- `.github/instructions/security.instructions.md` — updated (body replaced) +- `.github/instructions/unit-tests.instructions.md` — updated (body replaced) +- `.github/skills/plan-writing/SKILL.md` — updated (body replaced) + +**New — Copilot thin wrappers for maintenance skills:** + +- `.github/skills/ai-add-instruction/SKILL.md` — new +- `.github/skills/ai-add-prompt/SKILL.md` — new +- `.github/skills/ai-add-skill/SKILL.md` — new + +**Updated — Claude:** + +- `CLAUDE.md` — Conventions section updated to use `@imports` + +**New — Claude thin wrappers:** + +- `.claude/skills/plan-writing.md` — new +- `.claude/skills/ai-add-instruction.md` — new +- `.claude/skills/ai-add-prompt.md` — new +- `.claude/skills/ai-add-skill.md` — new + +## Verification + +- Open VS Code with Copilot enabled; ask it to document a TypeScript function. + Confirm it follows the JSDoc/TSDoc standards from `.ai/instructions/jsdoc.md`. +- In Claude Code, invoke `/plan-writing`. Confirm it reads the shared skill and + produces a plan in `docs/plans/` following the correct format. +- Invoke `/ai-add-instruction` in Claude Code and confirm it creates the shared + file, updates `CLAUDE.md`, and creates the Copilot wrapper. +- Invoke the `ai-add-instruction` skill in Copilot and confirm the same outcome. +- Check that `.github/instructions/*.instructions.md` files contain only + frontmatter + a `#file:` line (no substantive content). +- Check that CLAUDE.md Conventions section contains only `@import` lines (no + inline bullet points). + +## Decisions + +**`.ai/` over `ai/` or `ai_customizations/`** — The repo already uses dotfile +directories for tooling configuration (`.github/`, `.claude/`). `.ai/` follows +the same convention: it is tooling configuration, not source code. The short +name avoids verbose path prefixes in every thin wrapper file. + +**Maintenance skills live in `.ai/skills/`** — The alternative was `.claude/skills/` +only, but that would require Claude to be set up to manage Copilot wrappers, and +vice versa. Placing them in `.ai/` makes them invocable from either system. +They are intentionally colocated with other skills rather than in a separate +`meta/` subfolder to keep the structure flat and consistent. + +**`code-review` excluded from Claude @imports** — The instruction file +explicitly states it is intended for Copilot code review agents, not coding +assistants. Importing it into CLAUDE.md would cause Claude to apply review-only +constraints (e.g. "only comment on modified code") when writing code, which is +not the intended behavior. + +**`plan-writing` excluded from Claude @imports** — CLAUDE.md already delegates +plan workflow context to `docs/development.md`. The plan-writing instruction +covers the same ground and importing it would duplicate guidance already visible +to Claude. + +**Copilot prompts/Claude commands both map to `.claude/skills/`** — Claude Code +has merged commands into skills (`.claude/skills/` is the current recommended +path). The distinction between Copilot prompts and Copilot skills is preserved +in the `.ai/` folder (separate `prompts/` and `skills/` subfolders) for semantic +clarity, but both collapse to `.claude/skills/` on the Claude side. + +**Future extraction** — The `ai-add-*` maintenance skills are intended to +eventually be extracted into a Claude plugin and into Copilot user-level skills +(`~/.copilot/skills/`). Keeping them in `.ai/skills/` for now means either AI +system can invoke them, and extraction is a simple move-and-remove operation. From 3ad2b3f44120088f9c804ea6007e7c96e6db5dfc Mon Sep 17 00:00:00 2001 From: Mark Jubenville <ioncache@gmail.com> Date: Thu, 21 May 2026 00:26:15 -0400 Subject: [PATCH 09/10] chore(dev): add verify-unresolved-pr-comments shared prompt --- .ai/prompts/verify-unresolved-pr-comments.md | 46 +++++++++++++++++++ .../skills/verify-unresolved-pr-comments.md | 1 + .../verify-unresolved-pr-comments.prompt.md | 9 ++++ 3 files changed, 56 insertions(+) create mode 100644 .ai/prompts/verify-unresolved-pr-comments.md create mode 100644 .claude/skills/verify-unresolved-pr-comments.md create mode 100644 .github/prompts/verify-unresolved-pr-comments.prompt.md diff --git a/.ai/prompts/verify-unresolved-pr-comments.md b/.ai/prompts/verify-unresolved-pr-comments.md new file mode 100644 index 0000000..bcfbf01 --- /dev/null +++ b/.ai/prompts/verify-unresolved-pr-comments.md @@ -0,0 +1,46 @@ +# Verify Unresolved PR Comments + +Fetch the active pull request using the available pull request tool or API and +refresh it first so you evaluate the latest comments and diff. + +Collect all unresolved review threads or unresolved review comments on the PR, +regardless of how the pull request tool or API represents them. Include feedback +from all authors (Copilot, human reviewers, etc.). Also include +requested-changes reviews, or the tool's equivalent blocking review feedback, +when they are still unresolved. Note the author on each row so it's clear who +raised each issue. + +For each unresolved comment, evaluate it against the current PR diff, changed +files, or patch content provided by the available tool or API, and the project +guidelines: + +- Read the relevant section of the diff carefully — do not rely on the comment + description alone. +- Apply the project's code-review instructions rules: only flag new/changed + code, never pre-existing issues. +- Apply security instructions: always flag security issues regardless of scope. +- Do not recommend defense-in-depth for scenarios that can't happen through + normal code paths. + +Produce a markdown table with these columns: + +| # | Author | File | Issue Summary | Severity | Fix? | Suggestion Valid? | Required Action / PR Response | +| --- | ------ | ---- | ------------- | -------- | ---- | ----------------- | ----------------------------- | + +Rules for each column: + +- **Severity**: High / Medium / Low based on impact (High = correctness bug or + security; Medium = reliability, reproducibility, or operational risk; Low = + cosmetic or edge-case tooling) +- **Fix?**: ✅ Fix or 🚫 No fix — If the suggestion is valid and targets new + code introduced in this PR, the answer is always ✅ Fix. "🚫 No fix" is only + for comments that are invalid, target pre-existing code unchanged by the PR, + or recommend defense-in-depth for impossible scenarios. Do not defer valid + issues on new code — fix them now. +- **Suggestion Valid?**: ✅ Yes / ⚠️ Partially / ❌ No — with one-line reason if + Partially or No +- **Required Action**: If fixing, state the minimal change. If not fixing, write + the exact response to post in the PR thread. + +After the table, provide a one-line summary: how many to fix, how many to +respond-no-change, and any deferred items. diff --git a/.claude/skills/verify-unresolved-pr-comments.md b/.claude/skills/verify-unresolved-pr-comments.md new file mode 100644 index 0000000..3a6a110 --- /dev/null +++ b/.claude/skills/verify-unresolved-pr-comments.md @@ -0,0 +1 @@ +@.ai/prompts/verify-unresolved-pr-comments.md diff --git a/.github/prompts/verify-unresolved-pr-comments.prompt.md b/.github/prompts/verify-unresolved-pr-comments.prompt.md new file mode 100644 index 0000000..5b1e620 --- /dev/null +++ b/.github/prompts/verify-unresolved-pr-comments.prompt.md @@ -0,0 +1,9 @@ +--- +description: > + Analyze all unresolved review comments on the active PR and return a triage + chart with severity, fix recommendation, suggestion validity, and required + action. +agent: 'Plan' +--- + +#file:../../.ai/prompts/verify-unresolved-pr-comments.md From ef0ea008a4978486bc8e8783e3a42316e8122313 Mon Sep 17 00:00:00 2001 From: Mark Jubenville <ioncache@gmail.com> Date: Thu, 21 May 2026 00:32:31 -0400 Subject: [PATCH 10/10] fix: address CodeRabbit review comments - Update code-review.md to reference jsdoc-tsdoc (not jsdoc) and plan-writing, remove stale performance reference - Add text language identifier to fenced blocks in maintenance skills and plan doc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- .ai/instructions/code-review.md | 14 +++++++------- .ai/skills/ai-add-instruction.md | 2 +- .ai/skills/ai-add-prompt.md | 2 +- .ai/skills/ai-add-skill.md | 2 +- docs/plans/008-ai-customization-shared-layer.md | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.ai/instructions/code-review.md b/.ai/instructions/code-review.md index da19642..60c6cbc 100644 --- a/.ai/instructions/code-review.md +++ b/.ai/instructions/code-review.md @@ -59,11 +59,11 @@ Comments should be actionable and specific to what changed. ## Reference to Other Instruction Files -Other instruction files (`code-complexity`, `comments`, `jsdoc`, `performance`, -`security`, `unit-tests`) are primarily for coding agents implementing changes. -Use them as guidelines during reviews, but apply standards only to new or -modified code — don't flag pre-existing issues unless the change makes them -worse. +Other instruction files (`code-complexity`, `comments`, `jsdoc-tsdoc`, +`plan-writing`, `security`, `unit-tests`) are primarily for coding agents +implementing changes. Use them as guidelines during reviews, but apply standards +only to new or modified code — don't flag pre-existing issues unless the change +makes them worse. ## Handling Slight Regressions (Suggestion vs Requirement) @@ -80,8 +80,8 @@ requirements unless the regression is serious: deferring. When in doubt, suggest rather than require. Explain why for requirements; offer -examples for suggestions. Link to instruction files when relevant (`jsdoc`, -`performance`, `code-complexity`). +examples for suggestions. Link to instruction files when relevant (`jsdoc-tsdoc`, +`code-complexity`). ## Checklist diff --git a/.ai/skills/ai-add-instruction.md b/.ai/skills/ai-add-instruction.md index ec2905a..71caedc 100644 --- a/.ai/skills/ai-add-instruction.md +++ b/.ai/skills/ai-add-instruction.md @@ -53,7 +53,7 @@ Before creating any files, check: 4. **If Claude active** — Append to `CLAUDE.md`'s `## Conventions` section: - ``` + ```text @.ai/instructions/<name>.md ``` diff --git a/.ai/skills/ai-add-prompt.md b/.ai/skills/ai-add-prompt.md index d1f20b5..0629cf0 100644 --- a/.ai/skills/ai-add-prompt.md +++ b/.ai/skills/ai-add-prompt.md @@ -46,7 +46,7 @@ Before creating any files, check: 4. **If Claude active** — Create `.claude/skills/<name>.md`: - ``` + ```text @.ai/prompts/<name>.md ``` diff --git a/.ai/skills/ai-add-skill.md b/.ai/skills/ai-add-skill.md index 8e8c886..8e87186 100644 --- a/.ai/skills/ai-add-skill.md +++ b/.ai/skills/ai-add-skill.md @@ -49,7 +49,7 @@ Before creating any files, check: 4. **If Claude active** — Create `.claude/skills/<name>.md`: - ``` + ```text @.ai/skills/<name>.md ``` diff --git a/docs/plans/008-ai-customization-shared-layer.md b/docs/plans/008-ai-customization-shared-layer.md index 131999d..0c489d6 100644 --- a/docs/plans/008-ai-customization-shared-layer.md +++ b/docs/plans/008-ai-customization-shared-layer.md @@ -57,13 +57,13 @@ description: '...' **Claude skill** (`.claude/skills/<name>.md`) — no frontmatter: -``` +```text @.ai/skills/<name>.md ``` **Claude prompt** (`.claude/skills/<name>.md`) — no frontmatter: -``` +```text @.ai/prompts/<name>.md ``` @@ -135,7 +135,7 @@ Follow the standards in `.ai/instructions/plan-writing.md`. 8. Create `.claude/skills/plan-writing.md` containing only: - ``` + ```text @.ai/skills/plan-writing.md ```