UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

290 lines (289 loc) 8.64 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { prReviewRule } from "./pr-review-rule.js"; function violation(ruleId, ruleName, severity, message, file, line, suggestion) { return { ruleId, ruleName, severity, message, file, line, suggestion }; } function pass() { return { passed: true, violations: [] }; } function fail(violations) { return { passed: false, violations }; } function escapeGlobPart(part) { return part.replace(/\./g, "\\.").replace( /\{([^}]+)\}/g, (_m, choices) => `(${choices.split(",").join("|")})` ).replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]"); } function globToRegex(pattern) { if (pattern.startsWith("**/")) { const rest = escapeGlobPart(pattern.slice(3)); return new RegExp(`^(?:.+/)?${rest}$`); } const parts = pattern.split("/**/"); if (parts.length === 1) { return new RegExp(`^${escapeGlobPart(pattern)}$`); } const regexParts = parts.map(escapeGlobPart); return new RegExp(`^${regexParts.join("(?:/.*?/|/)")}$`); } function matchesScope(filePath, scope) { if (scope === "**/*" || scope === "*") return true; const re = globToRegex(scope); return re.test(filePath); } function filterByScope(files, scope) { return files.filter((f) => matchesScope(f, scope)); } const noCoauthor = { id: "no-coauthor", name: "No Co-Authored-By", description: "Block Co-Authored-By lines in commit messages", trigger: "commit", severity: "error", scope: "*", enabled: true, builtin: true, check(ctx) { if (!ctx.commitMessage) return pass(); if (/co-authored-by/i.test(ctx.commitMessage)) { return fail([ violation( this.id, this.name, this.severity, "Commit message contains Co-Authored-By line", void 0, void 0, "Remove the Co-Authored-By trailer" ) ]); } return pass(); } }; const noJestGlobals = { id: "no-jest-globals", name: "No @jest/globals imports", description: "Flag @jest/globals imports in src/ tests (causes redeclaration errors)", trigger: "lint", severity: "error", scope: "src/**/*.test.{ts,js}", enabled: true, builtin: true, check(ctx) { const violations = []; const files = filterByScope(ctx.files, this.scope); for (const file of files) { const content = ctx.content.get(file); if (!content) continue; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line && /@jest\/globals/.test(line)) { violations.push( violation( this.id, this.name, this.severity, `Import from @jest/globals found \u2014 use global jest instead`, file, i + 1, "Remove the import; jest/describe/it/expect are globally available" ) ); } } } return violations.length > 0 ? fail(violations) : pass(); } }; const catchNoUnderscore = { id: "catch-no-underscore", name: "Catch without underscore prefix", description: "Enforce catch {} not catch (_err) {} \u2014 underscore prefix not in allowed ESLint pattern", trigger: "lint", severity: "warn", scope: "src/**/*.{ts,js}", enabled: true, builtin: true, check(ctx) { const violations = []; const files = filterByScope(ctx.files, this.scope); for (const file of files) { const content = ctx.content.get(file); if (!content) continue; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line && /catch\s*\(\s*_\w*\s*\)/.test(line)) { violations.push( violation( this.id, this.name, this.severity, "catch with underscore-prefixed variable", file, i + 1, "Use catch {} (empty) or catch (err) {} (without underscore)" ) ); } } } return violations.length > 0 ? fail(violations) : pass(); } }; const THROW_EXCLUDE_PATTERNS = [ /middleware/i, /errors?\//i, /errors?\.(ts|js)$/i, /index\.(ts|js)$/, /\.test\.(ts|js)$/, /__tests__/ ]; const returnDontThrow = { id: "return-dont-throw", name: "Return undefined over throw", description: "Warn on throw in non-boundary code \u2014 prefer return undefined + log", trigger: "lint", severity: "info", scope: "src/**/*.{ts,js}", enabled: true, builtin: true, check(ctx) { const violations = []; const files = filterByScope(ctx.files, this.scope); for (const file of files) { if (THROW_EXCLUDE_PATTERNS.some((p) => p.test(file))) continue; const content = ctx.content.get(file); if (!content) continue; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line && /throw\s+new\s+/.test(line)) { violations.push( violation( this.id, this.name, this.severity, "throw statement in non-boundary code", file, i + 1, "Consider returning undefined and logging the error instead" ) ); } } } return violations.length > 0 ? fail(violations) : pass(); } }; const migrationSequential = { id: "migration-sequential", name: "Sequential migration numbering", description: "Validate migration files have no numbering gaps", trigger: "on-demand", severity: "error", scope: "**/migrations/*.sql", enabled: true, builtin: true, check(ctx) { const files = filterByScope(ctx.files, this.scope); const numbers = []; for (const file of files) { const basename = file.split("/").pop() ?? ""; const match = /^(\d+)/.exec(basename); if (match?.[1]) { numbers.push(parseInt(match[1], 10)); } } if (numbers.length < 2) return pass(); numbers.sort((a, b) => a - b); const violations = []; for (let i = 1; i < numbers.length; i++) { const prev = numbers[i - 1]; const curr = numbers[i]; if (curr - prev > 1) { violations.push( violation( this.id, this.name, this.severity, `Migration gap: ${String(prev).padStart(3, "0")} \u2192 ${String(curr).padStart(3, "0")} (missing ${curr - prev - 1} file(s))`, void 0, void 0, `Add migration(s) for numbers ${prev + 1}\u2013${curr - 1}` ) ); } } return violations.length > 0 ? fail(violations) : pass(); } }; const mockLifecycle = { id: "mock-lifecycle", name: "Mock lifecycle in tests", description: "Warn if clearAllMocks() is called without re-setting mocks in beforeEach", trigger: "lint", severity: "warn", scope: "src/**/*.test.{ts,js}", enabled: true, builtin: true, check(ctx) { const violations = []; const files = filterByScope(ctx.files, this.scope); for (const file of files) { const content = ctx.content.get(file); if (!content) continue; const hasClearAll = /clearAllMocks\(\)/.test(content); if (!hasClearAll) continue; const hasBeforeEach = /beforeEach/.test(content); const hasMockSetup = /mock(ReturnValue|ResolvedValue|Implementation)\s*\(/.test(content); if (hasClearAll && hasBeforeEach && !hasMockSetup) { violations.push( violation( this.id, this.name, this.severity, "clearAllMocks() used but no mock re-setup found (mockReturnValue/mockResolvedValue/mockImplementation)", file, void 0, "Re-set mock return values in beforeEach after clearAllMocks resets them" ) ); } } return violations.length > 0 ? fail(violations) : pass(); } }; const BUILT_IN_RULES = [ noCoauthor, noJestGlobals, catchNoUnderscore, returnDontThrow, migrationSequential, mockLifecycle, prReviewRule ]; function getBuiltinRows() { return BUILT_IN_RULES.map((r) => ({ id: r.id, name: r.name, description: r.description, trigger_type: r.trigger, severity: r.severity, scope: r.scope, enabled: r.enabled ? 1 : 0, builtin: r.builtin ? 1 : 0 })); } export { BUILT_IN_RULES, filterByScope, getBuiltinRows, matchesScope };