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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 42 additions & 40 deletions .archgate/adrs/ARCH-001-command-structure.rules.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
import { defineRules } from "../../src/formats/rules";
/// <reference path="../rules.d.ts" />

export default defineRules({
"register-function-export": {
description: "Command files must export a register*Command function",
async check(ctx) {
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
const checks = files.map(async (file) => {
const content = await ctx.readFile(file);
if (!/export\s+function\s+register\w+Command/.test(content)) {
ctx.report.violation({
message: "Command file must export a register*Command function",
file,
});
}
});
await Promise.all(checks);
export default {
rules: {
"register-function-export": {
description: "Command files must export a register*Command function",
async check(ctx) {
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
const checks = files.map(async (file) => {
const content = await ctx.readFile(file);
if (!/export\s+function\s+register\w+Command/.test(content)) {
ctx.report.violation({
message: "Command file must export a register*Command function",
file,
});
}
});
await Promise.all(checks);
},
},
},
"no-business-logic": {
description: "Command files should not contain business logic patterns",
severity: "error",
async check(ctx) {
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
const matches = await Promise.all(
files.map((file) =>
ctx.grep(
file,
/\.(parse|match|replace|split)\(.*\).*\.(parse|match|replace|split)\(/
"no-business-logic": {
description: "Command files should not contain business logic patterns",
severity: "error",
async check(ctx) {
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
const matches = await Promise.all(
files.map((file) =>
ctx.grep(
file,
/\.(parse|match|replace|split)\(.*\).*\.(parse|match|replace|split)\(/
)
)
)
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message:
"Complex data transformations should be in helpers, not command files",
file: m.file,
line: m.line,
fix: "Move transformation logic to a helper in src/helpers/ or src/formats/",
});
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message:
"Complex data transformations should be in helpers, not command files",
file: m.file,
line: m.line,
fix: "Move transformation logic to a helper in src/helpers/ or src/formats/",
});
}
}
}
},
},
},
});
} satisfies RuleSet;
140 changes: 72 additions & 68 deletions .archgate/adrs/ARCH-002-error-handling.rules.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,82 @@
import { defineRules } from "../../src/formats/rules";
/// <reference path="../rules.d.ts" />

export default defineRules({
"use-log-error": {
description:
"Use logError() instead of console.error() for user-facing errors",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) => !f.endsWith("helpers/log.ts") && !f.includes("tests/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, /console\.error\(/))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message:
"Use logError() from helpers/log.ts instead of console.error()",
file: m.file,
line: m.line,
fix: "Import { logError } from '../helpers/log' and use logError()",
});
export default {
rules: {
"use-log-error": {
description:
"Use logError() instead of console.error() for user-facing errors",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) => !f.endsWith("helpers/log.ts") && !f.includes("tests/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, /console\.error\(/))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message:
"Use logError() from helpers/log.ts instead of console.error()",
file: m.file,
line: m.line,
fix: "Import { logError } from '../helpers/log' and use logError()",
});
}
}
}
},
},
},
"use-log-helpers": {
description:
"Use log helpers instead of console.log/warn/info in helper and engine files",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) =>
(f.includes("helpers/") || f.includes("engine/")) &&
!f.endsWith("helpers/log.ts") &&
!f.endsWith("engine/reporter.ts") &&
!f.endsWith("helpers/login-flow.ts") &&
!f.includes("tests/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, /console\.(log|warn|info)\s*\(/))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message:
"Use logInfo/logWarn/logDebug from helpers/log.ts instead of direct console output",
file: m.file,
line: m.line,
fix: "Import { logInfo, logWarn } from '../helpers/log' and use logInfo() or logWarn()",
});
"use-log-helpers": {
description:
"Use log helpers instead of console.log/warn/info in helper and engine files",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) =>
(f.includes("helpers/") || f.includes("engine/")) &&
!f.endsWith("helpers/log.ts") &&
!f.endsWith("engine/reporter.ts") &&
!f.endsWith("helpers/login-flow.ts") &&
!f.includes("tests/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, /console\.(log|warn|info)\s*\(/))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message:
"Use logInfo/logWarn/logDebug from helpers/log.ts instead of direct console output",
file: m.file,
line: m.line,
fix: "Import { logInfo, logWarn } from '../helpers/log' and use logInfo() or logWarn()",
});
}
}
}
},
},
},
"exit-code-convention": {
description: "Process.exit should use codes 0, 1, or 2 only",
async check(ctx) {
const matches = await Promise.all(
ctx.scopedFiles.map((file) => ctx.grep(file, /process\.exit\((\d+)\)/))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
const codeMatch = m.content.match(/process\.exit\((\d+)\)/);
if (codeMatch) {
const code = Number(codeMatch[1]);
if (code !== 0 && code !== 1 && code !== 2) {
ctx.report.violation({
message: `Exit code ${code} is not standard. Use 0 (success), 1 (failure), or 2 (internal error)`,
file: m.file,
line: m.line,
});
"exit-code-convention": {
description: "Process.exit should use codes 0, 1, or 2 only",
async check(ctx) {
const matches = await Promise.all(
ctx.scopedFiles.map((file) =>
ctx.grep(file, /process\.exit\((\d+)\)/)
)
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
const codeMatch = m.content.match(/process\.exit\((\d+)\)/);
if (codeMatch) {
const code = Number(codeMatch[1]);
if (code !== 0 && code !== 1 && code !== 2) {
ctx.report.violation({
message: `Exit code ${code} is not standard. Use 0 (success), 1 (failure), or 2 (internal error)`,
file: m.file,
line: m.line,
});
}
}
}
}
}
},
},
},
});
} satisfies RuleSet;
82 changes: 42 additions & 40 deletions .archgate/adrs/ARCH-003-output-formatting.rules.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,56 @@
import { defineRules } from "../../src/formats/rules";
/// <reference path="../rules.d.ts" />

const EMOJI_PATTERN =
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
const EMOJI_IN_STRING =
/["'`].*[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}].*["'`]/u;

export default defineRules({
"no-emoji-in-output": {
description: "CLI output must not contain emoji characters",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) => !f.includes("tests/") && !f.includes(".archgate/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, EMOJI_PATTERN))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
if (EMOJI_IN_STRING.test(m.content)) {
export default {
rules: {
"no-emoji-in-output": {
description: "CLI output must not contain emoji characters",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) => !f.includes("tests/") && !f.includes(".archgate/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, EMOJI_PATTERN))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
if (EMOJI_IN_STRING.test(m.content)) {
ctx.report.violation({
message: "Do not use emoji in CLI output strings",
file: m.file,
line: m.line,
fix: "Remove emoji from output strings",
});
}
}
}
},
},
"use-style-text": {
description: "Use styleText from node:util instead of raw ANSI codes",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) => !f.includes("tests/") && !f.includes(".archgate/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, /\\u001b\[|\\x1b\[|\\033\[/))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message: "Do not use emoji in CLI output strings",
message:
"Use styleText() from node:util instead of raw ANSI escape codes",
file: m.file,
line: m.line,
fix: "Remove emoji from output strings",
fix: "Import { styleText } from 'node:util' and use styleText(style, text)",
});
}
}
}
},
},
"use-style-text": {
description: "Use styleText from node:util instead of raw ANSI codes",
async check(ctx) {
const files = ctx.scopedFiles.filter(
(f) => !f.includes("tests/") && !f.includes(".archgate/")
);
const matches = await Promise.all(
files.map((file) => ctx.grep(file, /\\u001b\[|\\x1b\[|\\033\[/))
);
for (const fileMatches of matches) {
for (const m of fileMatches) {
ctx.report.violation({
message:
"Use styleText() from node:util instead of raw ANSI escape codes",
file: m.file,
line: m.line,
fix: "Import { styleText } from 'node:util' and use styleText(style, text)",
});
}
}
},
},
},
});
} satisfies RuleSet;
42 changes: 23 additions & 19 deletions .archgate/adrs/ARCH-004-no-barrel-files.rules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineRules } from "../../src/formats/rules";
/// <reference path="../rules.d.ts" />

/**
* Determines whether a file is a barrel (re-export-only index.ts).
Expand Down Expand Up @@ -37,25 +37,29 @@ function isBarrelFile(content: string): boolean {
);
}

export default defineRules({
"no-barrel-files": {
description: "index.ts files must not be pure re-export barrels",
severity: "error",
async check(ctx) {
const indexFiles = ctx.scopedFiles.filter((f) => f.endsWith("/index.ts"));
export default {
rules: {
"no-barrel-files": {
description: "index.ts files must not be pure re-export barrels",
severity: "error",
async check(ctx) {
const indexFiles = ctx.scopedFiles.filter((f) =>
f.endsWith("/index.ts")
);

const checks = indexFiles.map(async (file) => {
const content = await ctx.readFile(file);
if (isBarrelFile(content)) {
ctx.report.violation({
message: `Barrel file detected: ${file} contains only re-exports and no logic. Import directly from source modules instead.`,
file,
fix: "Delete this barrel file and update all imports to point directly to the source module (e.g., import from './adr' instead of '.')",
});
}
});
const checks = indexFiles.map(async (file) => {
const content = await ctx.readFile(file);
if (isBarrelFile(content)) {
ctx.report.violation({
message: `Barrel file detected: ${file} contains only re-exports and no logic. Import directly from source modules instead.`,
file,
fix: "Delete this barrel file and update all imports to point directly to the source module (e.g., import from './adr' instead of '.')",
});
}
});

await Promise.all(checks);
await Promise.all(checks);
},
},
},
});
} satisfies RuleSet;
Loading
Loading