원클릭으로
project-code-review
Review code according to project standards
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Review code according to project standards
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Selects and vets SaaS vendors, libraries, frameworks, build tools, local executables, and replacement dependencies through hard gates, source audits, validation, and scored comparison. Use when choosing, recommending, evaluating, vetting, replacing, or comparing technologies or vendors for this repo.
Use when investigating an external tool's behavior, bug, quirk, capability gap, or fix difficulty; proactively write the troubleshooting doc the moment you finish diagnosing or working around one, even when the user did not ask; also use when writing or updating a doc/troubleshooting/<topic>.md file. The write-up is a required completion step, not an offer.
Use when writing manual steps a user must execute (clicks, keys) after bridges failed.
Use when writing or reviewing tests in this monorepo using the module-test harness.
Use when editing CSS, writing component styles, or reviewing CSS in this repo.
Use when writing or editing source files in non-TypeScript general-purpose languages.
| name | project-code-review |
| description | Review code according to project standards |
Structured code review that checks changes for correctness, type safety, security, style, and maintainability. Produces actionable findings categorized by severity.
All underlying rules referenced below are defined in AGENTS. md.
Read the diff or files under review, then evaluate each category below. Skip categories that do not apply to the language or change.
// Bad -- flag as WARNING
const lastItem = items[items.length];
// Good
const lastItem = items.at(-1,);
// Bad -- flag as WARNING: skips the last element
for (let index = 0; index < items.length - 1; index++) { ... }
// Good
// Looping is unavoidable here because each item has side effects
for (const item of items) { ... }
// Bad -- flag as BLOCKER
function getUser(id: string,): User {
const user = users.find(candidate => candidate.id === id);
return user; // may be undefined
}
// Good
function getUser(id: string,): User {
return notNullishOrThrow(users.find(function matchesId(candidate,) {
return candidate.id === id;
},),);
}
// Bad -- flag as WARNING: shared mutable state with concurrent access
const cache: Map<string, Data> = new Map();
async function getData(key: string,): Promise<Data> {
if (!cache.has(key,)) {
const data = await fetchData(key,);
cache.set(key, data,); // another call may have set it while awaiting
}
return notNullishOrThrow(cache.get(key,),);
}
// Bad -- flag as WARNING: swallowed exception
try {
await saveRecord(record,);
}
catch {
// silently ignored
}
// Good
try {
await saveRecord(record,);
}
catch (error) {
console.error('Failed to save record:', error,);
throw new Error('Failed to save record', { cause: error, },);
}
unknown over any;
no bare Function type!);
use assertion functions or narrowingsatisfies over as when validating shape without wideningconst generic parameters;
readonly array parameterstypeof === 'symbol' before identity checks// Bad -- flag as WARNING
function parseConfig(raw: string,) {
return JSON.parse(raw,);
}
// Good
function parseConfig(raw: string,): Config {
return JSON.parse(raw,) as Config;
}
unknown over any// Bad -- flag as WARNING
function processInput(data: any): void { ... }
// Bad -- flag as WARNING: bare Function type
function runCallback(callback: Function): void { ... }
// Good
function processInput(data: unknown): void { ... }
// Good
function runCallback(callback: () => void): void { ... }
// Bad -- flag as NIT: primitive type loses domain meaning
function getUser({ id }: { id: string }): User { ... }
function setPermissions({ level }: { level: number }): void { ... }
// Good -- branded type for opaque identifiers
type UserId = string & { readonly __brand: 'UserId' };
function getUser({ id }: { id: UserId }): User { ... }
// Good -- union type for small finite sets
type PermissionLevel = 1 | 2 | 3;
function setPermissions({ level }: { level: PermissionLevel }): void { ... }
// Good -- template literal type for structured strings
type SemVer = `${number}.${number}.${number}`;
function parseVersion({ version }: { version: SemVer }): VersionInfo { ... }
satisfies// Bad -- flag as WARNING: as widens the type
const config = { host: 'localhost', port: 8080, } as ServerConfig;
// Good -- explicit type annotation on the variable when possible
const config: ServerConfig = { host: 'localhost', port: 8080, };
// Good -- satisfies when direct annotation is not possible (e.g. exported configs)
export default { host: 'localhost', port: 8080, } satisfies ServerConfig;
Flag code that compares a value to a specific symbol and assumes the else branch is non-symbol:
// Anti-pattern -- flag as BLOCKER
if (out === NO_LITERAL) {
// handle sentinel
}
else {
// BUG: else may still be a symbol from the union
use(out.parsed,);
}
// Correct pattern
if (typeof out === 'symbol') {
if (out === NO_LITERAL) {
// handle sentinel
}
else {
throw new Error(`Unexpected symbol: ${String(out,)}`,);
}
}
else {
use(out.parsed,);
}
Flag missing const or readonly modifiers:
// Bad -- flag as WARNING
function processItems<T extends { id: string; },>(items: T[],): T[];
// Good
function processItems<const T extends { id: string; },>(
items: readonly T[],
): readonly T[];
// Bad -- flag as WARNING
function myFn<const T,>(myArr: T[],): T[];
// Good
function myFn<const T,>(myArr: readonly T[],): T[];
Flag non-descriptive generic names:
// Bad -- flag as NIT
<T extends Record<string, unknown>>
// Good
<TUser extends Record<string, unknown>>
...args) in functions we control;
accept an array parameter insteadconst x = function() {} -- use a function declaration instead.map(),
.sort(),
event handlers)Flag function expressions assigned to variables:
// Bad -- flag as WARNING
const greet = function greet(name: string,): void {};
const greet = function(name: string,): void {};
// Good -- use a function declaration
function greet(name: string,): void {}
Flag any function declaration or named arrow function with 2+ positional parameters:
// Bad -- flag as WARNING
function clamp(value: number, min: number, max: number,): number {
return Math.max(min, Math.min(max, value,),);
}
// Good
function clamp(
{ value, min, max, }: { value: number; min: number; max: number; },
): number {
return Math.max(min, Math.min(max, value,),);
}
// Bad -- flag as WARNING
function fetchUser(id: string, signal: AbortSignal): Promise<User> { ... }
// Good
function fetchUser({ id, signal }: { id: string; signal: AbortSignal }): Promise<User> { ... }
Callbacks passed to external APIs are exempt because the caller dictates the signature:
// OK -- signature dictated by Array.prototype.map
const doubled = items.map(function doubleByIndex(item, index,) {
return multiply({ value: item, by: index, },);
},);
// OK -- signature dictated by Array.prototype.sort
const sorted = items.sort(function byPriority(left, right,) {
return left.priority - right.priority;
},);
Flag function calls that appear before the function's declaration in source order. Hoisting makes this legal at runtime but breaks top-down readability:
// Bad -- flag as WARNING: b() called before its declaration
const a = b();
function b(): string {
return 'hello';
}
// Good -- declaration before use
function b(): string {
return 'hello';
}
const a = b();
Flag rest parameters in functions we control:
// Bad -- flag as WARNING
function logMessages(...messages: readonly string[]): void { ... }
// Good
function logMessages({ messages }: { messages: readonly string[] }): void { ... }
const over let;
no unnecessary mutationmap/filter/reduce) over imperative loopsfor...of when iteration is unavoidable,
never classic for loops1 / (2 * 2) for 1/4,
(16 - 2 - 1) / 16 for 13/16),
do not flag the expression as a readability issueconst over let// Bad -- flag as WARNING
let baseUrl = 'https://api.example.com';
// Good
const baseUrl = 'https://api.example.com';
// Bad -- flag as WARNING: accumulator mutated via let
let total = 0;
for (const item of items)
total += item.price;
// Good
const total = items.reduce(function addPrice(sum, item,) {
return sum + item.price;
}, 0,);
// Bad -- flag as WARNING
if (retries > 3) { ... }
await wait(5000);
// Good
const maxRetries = 3;
const retryDelayMs = 5000;
if (retries > maxRetries) { ... }
await wait(retryDelayMs);
// OK -- exempt-range composition in a named constant; do NOT flag
const BORDER_RADIUS = 1 / (2 * 2);
const FONT_SIZE = (16 - 2 - 1) / 16;
for...of over classic for// Bad -- flag as WARNING
for (let index = 0; index < items.length; index++)
process(items[index],);
// Good (when functional patterns do not apply)
// Iteration is unavoidable because process() has side effects
for (const item of items)
process(item,);
Flag imperative patterns when functional alternatives exist:
// Bad -- flag as WARNING
let results = [];
for (let i = 0; i < items.length; i++) {
if (items[i].isActive)
results.push(items[i].value * 2,);
}
// Good
const results = items
.filter(function isActive(item,) {
return item.isActive;
},)
.map(function doubleValue(item,) {
return item.value * 2;
},);
Flag for...in loops on objects:
// Bad -- flag as WARNING
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key,))
result[key] = process(obj[key],);
}
// Good
Object.entries(obj,).forEach(function applyProcess([key, value,],) {
result[key] = process(value,);
},);
// Good (for transformations)
const result = Object.fromEntries(
Object.entries(obj,).map(function processEntry([key, value,],) {
return [key, process(value,),];
},),
);
switch statementsFlag all switch statements.
Use if/else chains for branching or Record lookups for mapping a discriminant to a value.
switch adds break boilerplate,
risks accidental fallthrough,
and encourages large blocks.
If/else works naturally with early returns and needs no special syntax.
When the logic is a pure mapping from key to value,
a Record is more declarative and type-safe.
// Bad -- flag as WARNING: switch statement
switch (toolName) {
case 'Read': {
return `Reading ${shortPath(filePath,)}`;
}
case 'Edit': {
return `Editing ${shortPath(filePath,)}`;
}
case 'Bash': {
return `Running ${shortCommand(command,)}`;
}
default: {
return toolName;
}
}
// Good -- if/else for branching with logic
if (toolName === 'Read')
return `Reading ${shortPath(filePath,)}`;
else if (toolName === 'Edit')
return `Editing ${shortPath(filePath,)}`;
else if (toolName === 'Bash')
return `Running ${shortCommand(command,)}`;
else
return toolName;
// Good -- Record lookup for pure mappings
const TOOL_LABELS: Record<string, (input: ToolInput,) => string> = {
Read: function readLabel({ filePath, },) {
return `Reading ${shortPath(filePath,)}`;
},
Edit: function editLabel({ filePath, },) {
return `Editing ${shortPath(filePath,)}`;
},
Bash: function bashLabel({ command, },) {
return `Running ${shortCommand(command,)}`;
},
};
const labelFn = TOOL_LABELS[toolName];
return labelFn !== undefined ? labelFn(input,) : toolName;
Flag inline complex conditions:
// Bad -- flag as NIT
if (status === 'pending' && retries < maxRetries && !isTimeout) {
// retry logic
}
// Good
function canRetry(): boolean {
return status === 'pending' && retries < maxRetries && !isTimeout;
}
if (canRetry()) {
// retry logic
}
// Bad -- flag as WARNING
for (let i = 0; i < items.length; i++)
// Good
for (let itemIndex = 0; itemIndex < items.length; itemIndex++)
items.forEach(function processItem(item, itemIndex) { ... })
// Bad -- flag as BLOCKER
const apiKey = 'sk-live-abc123def456';
// Good
const apiKey = notNullishOrThrow(process.env['API_KEY'],);
// Bad -- flag as BLOCKER: shell injection
const output = execSync(`grep ${userQuery} /var/log/app.log`,);
// Good
const output = execSync('grep', [userQuery, '/var/log/app.log',],);
// Bad -- flag as BLOCKER: SQL injection
const rows = db.query(`SELECT * FROM users WHERE name = '${name}'`,);
// Good
const rows = db.query('SELECT * FROM users WHERE name = ?', [name,],);
// Bad -- flag as BLOCKER
console.log('Authenticating with token:', token,);
// Good
const tokenPrefixLength = 4;
console.log('Authenticating with token:',
token.slice(0, tokenPrefixLength,) + '...',);
@param,
@returns,
@example;
do not skip declarations that seem "obvious from context"@throws only on functions that actually throw;
do not add @throws to functions that never throwEmbed comments inside template literals using the ${''} trick.
Do not use target-language comments (XML,
HTML,
etc.) or move the comment outside the template.
Reasons:
// Bad -- flag as NIT: target-language comment
const xml = `
<os><type arch='x86_64'>hvm</type></os>
<!-- Guest CPU requires AVX -->
<cpu mode='host-passthrough'/>
`;
// Bad -- flag as NIT: comment disconnected from the line it explains
// Guest CPU requires AVX
const xml = `
<os><type arch='x86_64'>hvm</type></os>
<cpu mode='host-passthrough'/>
`;
// Good
const xml = `
<os><type arch='x86_64'>hvm</type></os>
${
// Guest CPU requires AVX
''}
<cpu mode='host-passthrough'/>
`;
Flag WHY-less comments:
// Bad -- flag as NIT
/** Line counter starting at 1 */
// Good
/** Mutable counter needed to track newlines encountered while scanning */
Flag TSDoc on non-declarations:
// Bad -- flag as WARNING: TSDoc on a statement
/** Increment the counter */
counter++;
// Good
// Increment the counter to account for the trailing newline
counter++;
Flag unescaped */ inside TSDoc blocks:
// Bad -- flag as BLOCKER: premature comment termination
/**
* Returns a string like "/* comment */"
*/
// Good
/**
* Returns a string like "/* comment *\\/"
*/
async/await over raw promises or callbacks.then(),
.catch(),
.finally() -- use async/await with try/catch or let errors propagate naturally by throwingPromise.all for independent concurrent operationsPromise.allSettled when partial failure is acceptableawait in loops without justification and eslint-disable commentAbortController for cancellable operations.then()/.catch()/.finally()// Bad -- flag as WARNING
function loadConfig(): Promise<Config> {
return readFile('config.json', 'utf8',)
.then(raw => JSON.parse(raw,) as Config)
.catch(error => {
console.error('Failed:', error,);
throw error;
},);
}
// Good
async function loadConfig(): Promise<Config> {
const raw = await readFile('config.json', 'utf8',);
return JSON.parse(raw,) as Config;
}
await in loops// Bad -- flag as WARNING: sequential when concurrency is possible
for (const url of urls) {
const response = await fetch(url,);
results.push(await response.json(),);
}
// Good
async function fetchJson(url: string,): Promise<unknown> {
const response = await fetch(url,);
return response.json();
}
const results = await Promise.all(urls.map(fetchJson,),);
Flag explicit new Promise when utilities exist:
// Bad -- flag as WARNING
function delay(ms,) {
return new Promise(function resolveAfterDelay(resolve,) {
setTimeout(resolve, ms,);
},);
}
// Good
import { wait, } from '@monochromatic-dev/module-es';
// Use wait(ms) directly
import type for type-only imports// Bad -- flag as WARNING: ungrouped, missing extension, default import, missing import type
import { readFile, } from 'node:fs/promises';
import { z, } from 'zod';
import config from './config';
import { Config, } from './types';
// Good
import { readFile, } from 'node:fs/promises';
import { z, } from 'zod';
import { parseConfig, } from '@monochromatic-dev/module-es';
import { loadSettings, } from './config.ts';
import type { Config, } from './types.ts';
try...finally -- use using/await using with Symbol.dispose/Symbol.asyncDispose for cleanupError for domain errorsprocess.exit();
throw insteadoutdent from @cspotcode/outdent@throws in TSDoc only for functions that actually throw;
flag @throws on non-throwing functions as NITnotNullishOrThrow instead of non-null assertion (!)console.error()using over try...finally// Bad -- flag as WARNING
const handle = openResource();
try {
await process(handle,);
}
finally {
handle.close();
}
// Good
await using handle = openResource();
await process(handle,);
process.exit()// Bad -- flag as WARNING
if (!isValid) {
console.error('Invalid input',);
process.exit(1,);
}
// Good
if (!isValid)
throw new Error('Invalid input',);
outdent for multi-line error messages// Bad -- flag as NIT
throw new Error(
'Failed to process record.\n'
+ `Expected type: ${expectedType}\n`
+ `Received type: ${receivedType}`,
);
// Good
import { outdent, } from '@cspotcode/outdent';
throw new Error(outdent`
Failed to process record.
Expected type: ${expectedType}
Received type: ${receivedType}
`,);
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
) {
super(message,);
this.name = 'ValidationError';
}
}
function isString(value: unknown,): value is string {
return typeof value === 'string';
}
function assertIsString(value: unknown,): asserts value is string {
if (typeof value !== 'string')
throw new TypeError('Expected string',);
}
// Bad -- flag as BLOCKER
const value = possiblyUndefined!;
// Good
import { notNullishOrThrow, } from '@monochromatic-dev/module-es';
const value = notNullishOrThrow(possiblyUndefined,);
Flag empty catch blocks without logging:
// Bad -- flag as WARNING
catch (error) { }
// Good -- fatal error
catch (error) {
throw new Error('Failed to get index stats', { cause: error });
}
// Good - expected error that shouldn't interrupt program operation.
catch (error) {
console.error('Failed to get index stats:', error);
}
Flag silently discarded unexpected states:
// Bad -- flag as BLOCKER
if (!(event instanceof CustomEvent))
return;
// Good
if (!(event instanceof CustomEvent))
throw new TypeError('Expected CustomEvent',);
<!-- Bad -- flag as WARNING -->
| Name | Type | Default |
| ---- | ------ | --------- |
| host | string | localhost |
| port | number | 8080 |
<!-- Good -->
- host -- `string`, default `localhost`
- port -- `number`, default `8080`
When reviewing PRs or multi-commit bundles, also review commit messages.
Conventional Commits:
<type>(<scope>): <subject> with optional body and footer.
Types:
feat,
fix,
docs,
style,
refactor,
perf,
test,
build,
ci,
chore,
revert.
Scope:
package name or * for multi-package changes.
feat(module-es): add a
Description of a
feat(module-es): enhance error handling utilities
error.assert.throw: add assertion-based error throwing
- Implement conditional error throwing based on assertions
- Include TypeScript type narrowing support
error.throw: add unified error throwing utility
- Implement consistent error creation
- Provide better stack traces
- Include custom error types
test(module-es): achieve 100% coverage for error utilities
- Add comprehensive test cases
- Use parameterized tests for edge cases
- Ensure proper type inference testing
When changes include CSS, check:
<dialog>,
Popover API,
CSS nesting)rem for all sizing;
calc() for derivation;
no px except device-pixel contextsleft/right/top/bottom)padding-inline,
margin-block,
border-radius,
inset,
gap)var() fallbacks (exception:
user-configurable)!important:focus-visible on interactive elements;
48px minimum touch targets/* Bad -- flag as WARNING: physical properties */
margin-left: 1rem;
padding-top: 0.5rem;
text-align: left;
top: 0;
right: 0;
/* Good */
margin-inline-start: 1rem;
padding-block-start: 0.5rem;
text-align: start;
inset-block-start: 0;
inset-inline-end: 0;
rem sizing/* Bad -- flag as WARNING */
font-size: 14px;
padding-block: 8px;
border-radius: 4px;
/* Good */
font-size: calc(14 / 16 * 1rem);
padding-block: calc(8 / 16 * 1rem);
border-radius: calc(4 / 16 * 1rem);
:focus-visible and touch targets/* Bad -- flag as WARNING: :focus instead of :focus-visible */
button:focus {
outline-color: var(--focus-ring);
}
/* Bad -- flag as WARNING: touch target too small */
button {
min-inline-size: 2rem;
min-block-size: 2rem;
}
/* Good */
button {
min-inline-size: 3rem; /* 48px equivalent */
min-block-size: 3rem;
&:focus-visible {
outline-color: var(--focus-ring);
}
}
/* Bad -- flag as WARNING: 4+ levels of nesting */
.card {
& .header {
& .title {
& span {
color: var(--accent-fg);
}
}
}
}
/* Good -- max 3 levels */
.card {
.title {
& span {
color: var(--accent-fg);
}
}
}
/* Bad -- flag as WARNING: multi-axis/multi-concept shorthands */
border: 1px solid #111;
padding: 0.5rem 1rem;
margin: 0 auto;
background: #fff url(...) no-repeat center;
/* Good -- longhand for multi-concept properties */
border-width: calc(1 / 16 * 1rem);
border-style: solid;
border-color: var(--gray-fg);
/* Good -- single-axis/single-concept shorthands are fine */
padding-block: 0.5rem;
padding-inline: 1rem;
margin-block: 0;
margin-inline: auto;
border-radius: 0.25rem;
inset: 0;
gap: 1rem;
/* Bad -- flag as WARNING */
color: #111;
border-color: #a00;
/* Good */
color: var(--gray-fg);
border-color: var(--error-fg);
<!-- Bad -- flag as NIT -->
<span class="pill pill--loading">
<!-- Good -->
<span class="pill" data-loading>
/* Bad */
.pill--loading {
opacity: 0.5;
}
/* Good */
.pill {
&[data-loading] {
opacity: 0.5;
}
}
tagged wrapper;
no raw console.* or untagged logger instancesmyFn.name or subsystem nametagged({ tag, l }) down the call chain// Bad -- flag as WARNING: untagged logger
export const l: Logger = $;
// Good
export const l: Logger = tagged({ tag: 'rss', },);
// Bad -- flag as WARNING: manual tag prefix
l.info('[cycle] capture complete',);
// Good
const l = tagged({ tag: 'cycle', l: parentLogger, },);
l.info('capture complete',);
// Bad -- flag as NIT: logger not re-tagged for sub-function
function processItem({ item, l, }: { item: Item; l: Logger; },): void {
l.info('processing',);
transformItem({ item, l, },);
}
// Good -- deep tagging
function processItem(
{ item, l: parentLogger, }: { item: Item; l: Logger; },
): void {
const l = tagged({ tag: processItem.name, l: parentLogger, },);
l.info('processing',);
transformItem({ item, l, },);
}
main() wrapper;
top-level await for asyncprocess.exit() -- throw errors insteadmain() wrapper// Bad -- flag as WARNING
async function main(): Promise<void> {
const data = await loadData();
console.log(data,);
}
main();
// Good
const data = await loadData();
console.log(data,);
Flag missing //region///endregion markers on substantial code blocks:
//region User Authentication Logic -- Handles user login, registration, and session management
function loginUser(credentials: UserCredentials,): UserSession {
return {} as UserSession;
}
function registerUser(details: UserDetails,): UserProfile {
return {} as UserProfile;
}
//endregion User Authentication Logic
Summary: <one-line description of the change and overall assessment>
Findings:
BLOCKER:
- <file:line> <description and suggested action>
WARNING:
- <file:line> <description and suggested action>
NIT:
- <file:line> <description and suggested action>
NON-ACTIONABLE:
- <file:line> <description and best-effort ideas>
Omit empty severity sections. If no issues found, write "No issues found" under Findings.
Every item in BLOCKER, WARNING, and NIT must include a concrete suggested action. If you spot a real problem but cannot determine a specific fix, place it under NON-ACTIONABLE with best-effort ideas -- still report it, just be honest about uncertainty.