Skip to content

Commit 74a0d54

Browse files
committed
chore: initial open source release
0 parents  commit 74a0d54

113 files changed

Lines changed: 9527 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
id: ARCH-001
3+
title: Command Structure
4+
domain: architecture
5+
rules: true
6+
files: ["src/commands/**/*.ts"]
7+
---
8+
9+
## Context
10+
11+
The CLI needs a consistent pattern for defining and registering commands. As the command surface grows (init, check, adr, mcp, upgrade, clean), the registration mechanism must scale without introducing hidden coupling or making the dependency graph opaque.
12+
13+
**Alternatives considered:**
14+
15+
- **Auto-discovery via `executableDir()`** — Commander.js supports automatic command discovery by scanning a directory for executable files. This eliminates manual imports but hides the dependency graph: adding or removing a command has no type-checked reference, making dead command detection impossible. It also requires each command to be a standalone executable, which prevents in-process testing and forces separate process spawning for every subcommand invocation.
16+
- **Plugin-based registration** — A plugin system where commands register themselves via a manifest or hook (similar to Oclif or Clipanion). This adds flexibility for third-party extensions but introduces significant complexity for an internal CLI with a known, finite set of commands. The indirection makes it harder to trace which code handles which command.
17+
- **Single-file command map** — Define all commands in a single file as a map of name-to-handler. Simple but creates a monolithic file that grows with every command, making merge conflicts frequent and readability poor.
18+
19+
The explicit register pattern strikes the right balance: each command owns its registration logic, the entry point makes all commands visible at a glance, and in-process execution enables straightforward testing without process spawning.
20+
21+
## Decision
22+
23+
Commands live in `src/commands/` and export a `register*Command(program)` function. The main entry point (`src/cli.ts`) explicitly imports and calls each register function. Subcommands (e.g., `adr create`, `adr list`) use nested directories with an `index.ts` that composes the subcommand group.
24+
25+
**Key constraints:**
26+
27+
1. **One command per file** — Each `.ts` file in `src/commands/` defines exactly one command (or one command group via its `index.ts`)
28+
2. **Explicit registration** — Every command must be manually imported and registered in `src/cli.ts`. No auto-discovery.
29+
3. **Thin commands** — Command files handle I/O only: parse arguments, call engine/helpers, format output. No business logic.
30+
4. **In-process execution** — Commands run in the same Bun process as the CLI entry point. No child process spawning.
31+
32+
## Do's and Don'ts
33+
34+
### Do
35+
36+
- Export a `register*Command` function from each command module
37+
- Keep commands thin: parse args, call helpers/engine, format output
38+
- Use `src/commands/<name>.ts` for top-level commands
39+
- Use `src/commands/<name>/index.ts` for command groups with subcommands
40+
- Import the register function explicitly in `src/cli.ts`
41+
42+
### Don't
43+
44+
- Don't put business logic in command files — move it to `src/engine/`, `src/helpers/`, or `src/formats/`
45+
- Don't use `executableDir()` for command discovery
46+
- Don't call `.parse()` in command files — the entry point handles parsing
47+
- Don't create commands that spawn child processes for subcommand execution
48+
49+
## Implementation Pattern
50+
51+
### Good Example
52+
53+
```typescript
54+
// src/commands/check.ts — thin command that delegates to engine
55+
import type { Command } from "@commander-js/extra-typings";
56+
import { loadRuleAdrs } from "../engine/loader";
57+
import { runChecks } from "../engine/runner";
58+
import { reportConsole, reportJSON, getExitCode } from "../engine/reporter";
59+
import { logError } from "../helpers/log";
60+
61+
export function registerCheckCommand(program: Command) {
62+
program
63+
.command("check")
64+
.description("Run automated ADR compliance checks")
65+
.option("--json", "Output results as JSON")
66+
.option("--staged", "Only check staged files")
67+
.action(async (opts) => {
68+
const adrs = await loadRuleAdrs();
69+
const results = await runChecks(adrs, { staged: opts.staged });
70+
if (opts.json) {
71+
reportJSON(results);
72+
} else {
73+
reportConsole(results);
74+
}
75+
process.exit(getExitCode(results));
76+
});
77+
}
78+
```
79+
80+
```typescript
81+
// src/cli.ts — explicit imports make all commands visible
82+
import { registerCheckCommand } from "./commands/check";
83+
import { registerInitCommand } from "./commands/init";
84+
import { registerAdrCommand } from "./commands/adr";
85+
86+
registerInitCommand(program);
87+
registerCheckCommand(program);
88+
registerAdrCommand(program);
89+
```
90+
91+
### Bad Example
92+
93+
```typescript
94+
// BAD: business logic inside command file
95+
export function registerCheckCommand(program: Command) {
96+
program.command("check").action(async () => {
97+
// Business logic should NOT be here
98+
const files = await glob("src/**/*.ts");
99+
for (const file of files) {
100+
const content = await Bun.file(file).text();
101+
const violations = content.match(/console\.error/g);
102+
// ... complex processing ...
103+
}
104+
});
105+
}
106+
```
107+
108+
### Subcommand Group Pattern
109+
110+
```typescript
111+
// src/commands/adr/index.ts — composes subcommand group (contains real logic)
112+
import type { Command } from "@commander-js/extra-typings";
113+
import { registerAdrCreateCommand } from "./create";
114+
import { registerAdrListCommand } from "./list";
115+
import { registerAdrShowCommand } from "./show";
116+
import { registerAdrUpdateCommand } from "./update";
117+
118+
export function registerAdrCommand(program: Command) {
119+
const adr = program
120+
.command("adr")
121+
.description("Manage Architecture Decision Records");
122+
123+
registerAdrCreateCommand(adr);
124+
registerAdrListCommand(adr);
125+
registerAdrShowCommand(adr);
126+
registerAdrUpdateCommand(adr);
127+
}
128+
```
129+
130+
## Consequences
131+
132+
### Positive
133+
134+
- **In-process execution enables testing** — Commands can be tested by calling `register*Command()` directly, without spawning subprocesses or mocking executables
135+
- **Explicit imports make dependencies clear** — Opening `src/cli.ts` shows every command the CLI supports. No hidden commands loaded at runtime.
136+
- **Subcommand nesting is straightforward** — Command groups use the same pattern as top-level commands, with an `index.ts` that composes children
137+
- **Type-safe registration** — Commander.js `@commander-js/extra-typings` provides full type inference for options and arguments within each register function
138+
139+
### Negative
140+
141+
- **Manual import bookkeeping** — Each new command requires adding an import and registration call in `src/cli.ts`. This is a minor overhead for a CLI with fewer than 15 commands.
142+
- **No hot-reload of commands** — Adding a new command requires restarting the CLI process. Acceptable for a development tool.
143+
144+
### Risks
145+
146+
- **Stale imports when commands are removed** — If a command file is deleted but its import in `src/cli.ts` is not removed, TypeScript will catch the error at compile time. The `bun run typecheck` step in the validation pipeline prevents this from reaching production.
147+
- **Command group index.ts confused with barrels** — The `index.ts` files in command group directories (e.g., `src/commands/adr/index.ts`) contain real composition logic, not re-exports. [ARCH-004 — No Barrel Files](./ARCH-004-no-barrel-files.md) explicitly permits `index.ts` files with logic.
148+
149+
## Compliance and Enforcement
150+
151+
### Automated Enforcement
152+
153+
- **Archgate rule** `ARCH-001/register-function-export`: Scans all command files under `src/commands/` (excluding `index.ts` group files) and verifies each exports a `register*Command` function. Severity: `error`.
154+
- **Archgate rule** `ARCH-001/no-business-logic`: Detects complex data transformation patterns in command files that should be in helpers. Severity: `error`.
155+
156+
### Manual Enforcement
157+
158+
Code reviewers MUST verify:
159+
160+
1. New commands are imported and registered in `src/cli.ts`
161+
2. Command files delegate to engine/helpers for business logic
162+
3. Command group `index.ts` files contain composition logic, not just re-exports
163+
164+
## References
165+
166+
- [Commander.js documentation](https://github.com/tj/commander.js)
167+
- [ARCH-004 — No Barrel Files](./ARCH-004-no-barrel-files.md) — Permits `index.ts` with logic, forbids re-export-only barrels
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { defineRules } from "../../src/formats/rules";
2+
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);
18+
},
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)\(/
30+
)
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+
});
42+
}
43+
}
44+
},
45+
},
46+
});
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
id: ARCH-002
3+
title: Error Handling
4+
domain: architecture
5+
rules: true
6+
files: ["src/**/*.ts"]
7+
---
8+
9+
# Error Handling
10+
11+
## Context
12+
13+
CLI tools must provide clear, actionable error messages without exposing internal details to users. Inconsistent error handling leads to confusing experiences: some errors show stack traces, others silently swallow failures, and exit codes are unpredictable for scripts and CI integrations.
14+
15+
**Alternatives considered:**
16+
17+
- **Try-catch everywhere** — Wrapping every function in try-catch blocks provides fine-grained error control but leads to deeply nested code and often results in errors being caught and swallowed unintentionally. Developers forget to re-throw or log, and errors disappear silently.
18+
- **Error middleware / centralized handler** — A single `process.on("uncaughtException")` handler catches all unhandled errors. This works for unexpected crashes but cannot distinguish between user errors (invalid input) and bugs (null pointer). All errors get the same treatment, losing the semantic distinction between "you made a mistake" and "we have a bug."
19+
- **Result types (Either/Result monad)** — Encoding success/failure in return types (e.g., `Result<T, E>`) makes error handling explicit. This is the most type-safe approach but adds significant ceremony for a CLI where most errors are terminal (print message, exit). The overhead is not justified when the error handling strategy is "tell the user and exit."
20+
21+
The three-tier exit code model provides a simple contract that covers all CLI use cases: success, expected failure, and unexpected crash. Combined with `logError()` for consistent formatting, this gives users and CI systems predictable behavior without overengineering.
22+
23+
## Decision
24+
25+
Use three exit codes with clear semantics:
26+
27+
| Exit Code | Meaning | When to Use |
28+
| --------- | ---------------- | ----------------------------------------------------------------------------- |
29+
| `0` | Success | Operation completed successfully |
30+
| `1` | Expected failure | Invalid input, missing config, ADR violations found, operation cannot proceed |
31+
| `2` | Internal error | Bugs, unhandled exceptions, unexpected crashes |
32+
33+
**Error output conventions:**
34+
35+
- User-facing errors use `logError()` from `src/helpers/log.ts`, which formats with `styleText("red", ...)` and writes to stderr
36+
- Actionable suggestions accompany error messages when possible (e.g., "Run `archgate init` to create a governance directory")
37+
- No stack traces for user-triggered errors (exit code 1)
38+
- Unexpected errors (exit code 2) may include stack traces when `DEBUG` or `TRACE` environment variables are set
39+
- All error output goes to stderr, never stdout (stdout is reserved for command output and `--json` results)
40+
41+
## Do's and Don'ts
42+
43+
### Do
44+
45+
- Use `logError()` from `src/helpers/log.ts` for user-facing errors
46+
- Exit with code 1 for expected failures (missing config, invalid input, violations found)
47+
- Let unexpected errors crash naturally (exit code 2)
48+
- Provide actionable suggestions in error messages
49+
- Write errors to stderr (via `logError()`), not stdout
50+
51+
### Don't
52+
53+
- Don't catch and swallow unexpected errors — let them propagate
54+
- Don't show stack traces for user errors
55+
- Don't use `console.error()` directly — use `logError()` for consistent formatting
56+
- Don't exit with code 0 when an operation fails
57+
- Don't use exit codes other than 0, 1, or 2
58+
59+
## Implementation Pattern
60+
61+
### Good Example
62+
63+
```typescript
64+
// Expected failure — user error with actionable suggestion
65+
import { logError } from "../helpers/log";
66+
67+
const adrsPath = resolve(projectRoot, ".archgate/adrs");
68+
if (!existsSync(adrsPath)) {
69+
logError(
70+
"No .archgate/ directory found. Run `archgate init` to initialize governance."
71+
);
72+
process.exit(1);
73+
}
74+
```
75+
76+
```typescript
77+
// Validation failure — report and exit with code 1
78+
const results = await runChecks(adrs);
79+
const exitCode = getExitCode(results); // 0 if clean, 1 if violations
80+
process.exit(exitCode);
81+
```
82+
83+
### Bad Example
84+
85+
```typescript
86+
// BAD: swallowing errors silently
87+
try {
88+
const config = await loadConfig();
89+
} catch {
90+
// Error is lost — caller has no idea something failed
91+
}
92+
93+
// BAD: using console.error directly
94+
console.error("Something went wrong"); // No consistent formatting
95+
96+
// BAD: non-standard exit code
97+
process.exit(42); // Scripts cannot interpret this
98+
99+
// BAD: showing stack trace for user error
100+
try {
101+
validateInput(args);
102+
} catch (e) {
103+
console.error(e); // Prints stack trace for simple validation failure
104+
process.exit(1);
105+
}
106+
```
107+
108+
## Consequences
109+
110+
### Positive
111+
112+
- **Consistent error experience** — Users always see the same error format regardless of which command fails
113+
- **Exit codes enable scripting** — CI systems and shell scripts can branch on 0/1/2 with clear semantics
114+
- **Clear separation between user errors and bugs** — Exit code 1 means "you need to fix something," exit code 2 means "we have a bug"
115+
- **Actionable messages reduce support burden** — Telling users what to do next prevents repeated "how do I fix this?" questions
116+
117+
### Negative
118+
119+
- **Debugging requires environment variables** — Detailed error context (stack traces, internal state) is only available with `DEBUG` or `TRACE` env vars. This is intentional but can slow down debugging for contributors unfamiliar with the convention.
120+
121+
### Risks
122+
123+
- **Swallowed errors in async code** — Async functions that catch errors without re-throwing can silently fail. Unhandled promise rejections in Bun terminate the process with a non-zero exit code, which provides a safety net, but the error message may be unclear.
124+
- **Mitigation:** The `logError()` convention makes explicit error handling visible in code review. The `use-log-error` automated rule flags direct `console.error()` usage, nudging developers toward the standard pattern.
125+
- **Exit code 2 masking real issues** — If an unexpected error occurs in a rule file, the CLI exits with code 2 ("internal error") rather than code 1 ("violations"). This could confuse CI systems that only check for non-zero exit.
126+
- **Mitigation:** The check engine wraps rule execution with timeout and error boundaries, reporting rule errors separately from violations. The `--verbose` flag shows which rules errored.
127+
128+
## Compliance and Enforcement
129+
130+
### Automated Enforcement
131+
132+
- **Archgate rule** `ARCH-002/use-log-error`: Scans all source files (excluding `helpers/log.ts` and test files) for `console.error()` usage and flags violations. Severity: `error`.
133+
- **Archgate rule** `ARCH-002/exit-code-convention`: Scans all source files for `process.exit()` calls and verifies the exit code is 0, 1, or 2. Severity: `error`.
134+
135+
### Manual Enforcement
136+
137+
Code reviewers MUST verify:
138+
139+
1. Error messages include actionable suggestions where possible
140+
2. Expected failures exit with code 1, not code 2
141+
3. No try-catch blocks that swallow errors without logging or re-throwing
142+
143+
## References
144+
145+
- [POSIX exit code conventions](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02)
146+
- [ARCH-003 — Output Formatting](./ARCH-003-output-formatting.md) — Complements this ADR with output conventions (stderr for errors, stdout for results)

0 commit comments

Comments
 (0)