@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
JavaScript
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
};