Skip to content

Commit 3455d69

Browse files
committed
feat: local rules shim, ARCH-010, and parallel file discovery (#86)
* feat: generate local rules.d.ts shim, remove archgate/rules npm export Rules files no longer need node_modules or package.json. The CLI generates an ambient .archgate/rules.d.ts on init/check so .rules.ts files get full editor IntelliSense via triple-slash references. - Remove defineRules() function and ./rules npm export - Generate ambient rules.d.ts (declare types, no export) on every check - Rule files use plain `export default { rules: {...} } satisfies RuleSet` - Scaffold companion .rules.ts on `adr create --rules` - Add .archgate/rules.d.ts to .gitignore during init - Missing companion files now throw instead of warning - Migrate all project rule files, tests, and docs to new pattern * docs: add note about triple-slash-reference linter conflict * feat: auto-configure linter overrides on init, add ARCH-010 - archgate init detects .oxlintrc.json and .eslintrc.json and adds an override to disable triple-slash-reference for .archgate/adrs/ - Add ARCH-010: prefer Bun.file().json() over JSON.parse() * fix: apply ARCH-010 to claude-settings.ts, improve rule detection - Replace JSON.parse(content) with Bun.file().json() in claude-settings - Extend ARCH-010 rule to catch two-line pattern (Bun.file().text() assigned to variable then passed to JSON.parse) * refactor: parallelize file discovery with Promise.all Replace sequential await-in-loop patterns with Promise.all for independent file reads in loader, context, and adr-writer. Removes all no-await-in-loop lint suppressions from these files. * refactor: parallelize ADR rules and git file resolution with Promise.all Remove no-await-in-loop suppressions from ARCH-010 and GEN-002 rule files and git-files.ts by replacing sequential loops with Promise.all.
1 parent e65de4c commit 3455d69

51 files changed

Lines changed: 2523 additions & 1822 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,48 @@
1-
import { defineRules } from "../../src/formats/rules";
1+
/// <reference path="../rules.d.ts" />
22

3-
export default defineRules({
4-
"register-function-export": {
5-
description: "Command files must export a register*Command function",
6-
async check(ctx) {
7-
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
8-
const checks = files.map(async (file) => {
9-
const content = await ctx.readFile(file);
10-
if (!/export\s+function\s+register\w+Command/.test(content)) {
11-
ctx.report.violation({
12-
message: "Command file must export a register*Command function",
13-
file,
14-
});
15-
}
16-
});
17-
await Promise.all(checks);
3+
export default {
4+
rules: {
5+
"register-function-export": {
6+
description: "Command files must export a register*Command function",
7+
async check(ctx) {
8+
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
9+
const checks = files.map(async (file) => {
10+
const content = await ctx.readFile(file);
11+
if (!/export\s+function\s+register\w+Command/.test(content)) {
12+
ctx.report.violation({
13+
message: "Command file must export a register*Command function",
14+
file,
15+
});
16+
}
17+
});
18+
await Promise.all(checks);
19+
},
1820
},
19-
},
20-
"no-business-logic": {
21-
description: "Command files should not contain business logic patterns",
22-
severity: "error",
23-
async check(ctx) {
24-
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
25-
const matches = await Promise.all(
26-
files.map((file) =>
27-
ctx.grep(
28-
file,
29-
/\.(parse|match|replace|split)\(.*\).*\.(parse|match|replace|split)\(/
21+
"no-business-logic": {
22+
description: "Command files should not contain business logic patterns",
23+
severity: "error",
24+
async check(ctx) {
25+
const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
26+
const matches = await Promise.all(
27+
files.map((file) =>
28+
ctx.grep(
29+
file,
30+
/\.(parse|match|replace|split)\(.*\).*\.(parse|match|replace|split)\(/
31+
)
3032
)
31-
)
32-
);
33-
for (const fileMatches of matches) {
34-
for (const m of fileMatches) {
35-
ctx.report.violation({
36-
message:
37-
"Complex data transformations should be in helpers, not command files",
38-
file: m.file,
39-
line: m.line,
40-
fix: "Move transformation logic to a helper in src/helpers/ or src/formats/",
41-
});
33+
);
34+
for (const fileMatches of matches) {
35+
for (const m of fileMatches) {
36+
ctx.report.violation({
37+
message:
38+
"Complex data transformations should be in helpers, not command files",
39+
file: m.file,
40+
line: m.line,
41+
fix: "Move transformation logic to a helper in src/helpers/ or src/formats/",
42+
});
43+
}
4244
}
43-
}
45+
},
4446
},
4547
},
46-
});
48+
} satisfies RuleSet;
Lines changed: 72 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,82 @@
1-
import { defineRules } from "../../src/formats/rules";
1+
/// <reference path="../rules.d.ts" />
22

3-
export default defineRules({
4-
"use-log-error": {
5-
description:
6-
"Use logError() instead of console.error() for user-facing errors",
7-
async check(ctx) {
8-
const files = ctx.scopedFiles.filter(
9-
(f) => !f.endsWith("helpers/log.ts") && !f.includes("tests/")
10-
);
11-
const matches = await Promise.all(
12-
files.map((file) => ctx.grep(file, /console\.error\(/))
13-
);
14-
for (const fileMatches of matches) {
15-
for (const m of fileMatches) {
16-
ctx.report.violation({
17-
message:
18-
"Use logError() from helpers/log.ts instead of console.error()",
19-
file: m.file,
20-
line: m.line,
21-
fix: "Import { logError } from '../helpers/log' and use logError()",
22-
});
3+
export default {
4+
rules: {
5+
"use-log-error": {
6+
description:
7+
"Use logError() instead of console.error() for user-facing errors",
8+
async check(ctx) {
9+
const files = ctx.scopedFiles.filter(
10+
(f) => !f.endsWith("helpers/log.ts") && !f.includes("tests/")
11+
);
12+
const matches = await Promise.all(
13+
files.map((file) => ctx.grep(file, /console\.error\(/))
14+
);
15+
for (const fileMatches of matches) {
16+
for (const m of fileMatches) {
17+
ctx.report.violation({
18+
message:
19+
"Use logError() from helpers/log.ts instead of console.error()",
20+
file: m.file,
21+
line: m.line,
22+
fix: "Import { logError } from '../helpers/log' and use logError()",
23+
});
24+
}
2325
}
24-
}
26+
},
2527
},
26-
},
27-
"use-log-helpers": {
28-
description:
29-
"Use log helpers instead of console.log/warn/info in helper and engine files",
30-
async check(ctx) {
31-
const files = ctx.scopedFiles.filter(
32-
(f) =>
33-
(f.includes("helpers/") || f.includes("engine/")) &&
34-
!f.endsWith("helpers/log.ts") &&
35-
!f.endsWith("engine/reporter.ts") &&
36-
!f.endsWith("helpers/login-flow.ts") &&
37-
!f.includes("tests/")
38-
);
39-
const matches = await Promise.all(
40-
files.map((file) => ctx.grep(file, /console\.(log|warn|info)\s*\(/))
41-
);
42-
for (const fileMatches of matches) {
43-
for (const m of fileMatches) {
44-
ctx.report.violation({
45-
message:
46-
"Use logInfo/logWarn/logDebug from helpers/log.ts instead of direct console output",
47-
file: m.file,
48-
line: m.line,
49-
fix: "Import { logInfo, logWarn } from '../helpers/log' and use logInfo() or logWarn()",
50-
});
28+
"use-log-helpers": {
29+
description:
30+
"Use log helpers instead of console.log/warn/info in helper and engine files",
31+
async check(ctx) {
32+
const files = ctx.scopedFiles.filter(
33+
(f) =>
34+
(f.includes("helpers/") || f.includes("engine/")) &&
35+
!f.endsWith("helpers/log.ts") &&
36+
!f.endsWith("engine/reporter.ts") &&
37+
!f.endsWith("helpers/login-flow.ts") &&
38+
!f.includes("tests/")
39+
);
40+
const matches = await Promise.all(
41+
files.map((file) => ctx.grep(file, /console\.(log|warn|info)\s*\(/))
42+
);
43+
for (const fileMatches of matches) {
44+
for (const m of fileMatches) {
45+
ctx.report.violation({
46+
message:
47+
"Use logInfo/logWarn/logDebug from helpers/log.ts instead of direct console output",
48+
file: m.file,
49+
line: m.line,
50+
fix: "Import { logInfo, logWarn } from '../helpers/log' and use logInfo() or logWarn()",
51+
});
52+
}
5153
}
52-
}
54+
},
5355
},
54-
},
55-
"exit-code-convention": {
56-
description: "Process.exit should use codes 0, 1, or 2 only",
57-
async check(ctx) {
58-
const matches = await Promise.all(
59-
ctx.scopedFiles.map((file) => ctx.grep(file, /process\.exit\((\d+)\)/))
60-
);
61-
for (const fileMatches of matches) {
62-
for (const m of fileMatches) {
63-
const codeMatch = m.content.match(/process\.exit\((\d+)\)/);
64-
if (codeMatch) {
65-
const code = Number(codeMatch[1]);
66-
if (code !== 0 && code !== 1 && code !== 2) {
67-
ctx.report.violation({
68-
message: `Exit code ${code} is not standard. Use 0 (success), 1 (failure), or 2 (internal error)`,
69-
file: m.file,
70-
line: m.line,
71-
});
56+
"exit-code-convention": {
57+
description: "Process.exit should use codes 0, 1, or 2 only",
58+
async check(ctx) {
59+
const matches = await Promise.all(
60+
ctx.scopedFiles.map((file) =>
61+
ctx.grep(file, /process\.exit\((\d+)\)/)
62+
)
63+
);
64+
for (const fileMatches of matches) {
65+
for (const m of fileMatches) {
66+
const codeMatch = m.content.match(/process\.exit\((\d+)\)/);
67+
if (codeMatch) {
68+
const code = Number(codeMatch[1]);
69+
if (code !== 0 && code !== 1 && code !== 2) {
70+
ctx.report.violation({
71+
message: `Exit code ${code} is not standard. Use 0 (success), 1 (failure), or 2 (internal error)`,
72+
file: m.file,
73+
line: m.line,
74+
});
75+
}
7276
}
7377
}
7478
}
75-
}
79+
},
7680
},
7781
},
78-
});
82+
} satisfies RuleSet;
Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,56 @@
1-
import { defineRules } from "../../src/formats/rules";
1+
/// <reference path="../rules.d.ts" />
22

33
const EMOJI_PATTERN =
44
/[\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;
55
const EMOJI_IN_STRING =
66
/["'`].*[\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;
77

8-
export default defineRules({
9-
"no-emoji-in-output": {
10-
description: "CLI output must not contain emoji characters",
11-
async check(ctx) {
12-
const files = ctx.scopedFiles.filter(
13-
(f) => !f.includes("tests/") && !f.includes(".archgate/")
14-
);
15-
const matches = await Promise.all(
16-
files.map((file) => ctx.grep(file, EMOJI_PATTERN))
17-
);
18-
for (const fileMatches of matches) {
19-
for (const m of fileMatches) {
20-
if (EMOJI_IN_STRING.test(m.content)) {
8+
export default {
9+
rules: {
10+
"no-emoji-in-output": {
11+
description: "CLI output must not contain emoji characters",
12+
async check(ctx) {
13+
const files = ctx.scopedFiles.filter(
14+
(f) => !f.includes("tests/") && !f.includes(".archgate/")
15+
);
16+
const matches = await Promise.all(
17+
files.map((file) => ctx.grep(file, EMOJI_PATTERN))
18+
);
19+
for (const fileMatches of matches) {
20+
for (const m of fileMatches) {
21+
if (EMOJI_IN_STRING.test(m.content)) {
22+
ctx.report.violation({
23+
message: "Do not use emoji in CLI output strings",
24+
file: m.file,
25+
line: m.line,
26+
fix: "Remove emoji from output strings",
27+
});
28+
}
29+
}
30+
}
31+
},
32+
},
33+
"use-style-text": {
34+
description: "Use styleText from node:util instead of raw ANSI codes",
35+
async check(ctx) {
36+
const files = ctx.scopedFiles.filter(
37+
(f) => !f.includes("tests/") && !f.includes(".archgate/")
38+
);
39+
const matches = await Promise.all(
40+
files.map((file) => ctx.grep(file, /\\u001b\[|\\x1b\[|\\033\[/))
41+
);
42+
for (const fileMatches of matches) {
43+
for (const m of fileMatches) {
2144
ctx.report.violation({
22-
message: "Do not use emoji in CLI output strings",
45+
message:
46+
"Use styleText() from node:util instead of raw ANSI escape codes",
2347
file: m.file,
2448
line: m.line,
25-
fix: "Remove emoji from output strings",
49+
fix: "Import { styleText } from 'node:util' and use styleText(style, text)",
2650
});
2751
}
2852
}
29-
}
30-
},
31-
},
32-
"use-style-text": {
33-
description: "Use styleText from node:util instead of raw ANSI codes",
34-
async check(ctx) {
35-
const files = ctx.scopedFiles.filter(
36-
(f) => !f.includes("tests/") && !f.includes(".archgate/")
37-
);
38-
const matches = await Promise.all(
39-
files.map((file) => ctx.grep(file, /\\u001b\[|\\x1b\[|\\033\[/))
40-
);
41-
for (const fileMatches of matches) {
42-
for (const m of fileMatches) {
43-
ctx.report.violation({
44-
message:
45-
"Use styleText() from node:util instead of raw ANSI escape codes",
46-
file: m.file,
47-
line: m.line,
48-
fix: "Import { styleText } from 'node:util' and use styleText(style, text)",
49-
});
50-
}
51-
}
53+
},
5254
},
5355
},
54-
});
56+
} satisfies RuleSet;

.archgate/adrs/ARCH-004-no-barrel-files.rules.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineRules } from "../../src/formats/rules";
1+
/// <reference path="../rules.d.ts" />
22

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

40-
export default defineRules({
41-
"no-barrel-files": {
42-
description: "index.ts files must not be pure re-export barrels",
43-
severity: "error",
44-
async check(ctx) {
45-
const indexFiles = ctx.scopedFiles.filter((f) => f.endsWith("/index.ts"));
40+
export default {
41+
rules: {
42+
"no-barrel-files": {
43+
description: "index.ts files must not be pure re-export barrels",
44+
severity: "error",
45+
async check(ctx) {
46+
const indexFiles = ctx.scopedFiles.filter((f) =>
47+
f.endsWith("/index.ts")
48+
);
4649

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

58-
await Promise.all(checks);
61+
await Promise.all(checks);
62+
},
5963
},
6064
},
61-
});
65+
} satisfies RuleSet;

0 commit comments

Comments
 (0)