Skip to content

Commit c011274

Browse files
authored
Merge branch 'main' into feat/ctx-read-yaml-check-case
2 parents f4ff986 + 9a114b3 commit c011274

151 files changed

Lines changed: 1463 additions & 1879 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.

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

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
/// <reference path="../rules.d.ts" />
22

33
/**
4-
* A Program body is barrel-shaped when every top-level statement is purely
5-
* import/re-export plumbing:
6-
* - ImportDeclaration — `import { x } from "./y"` / `import "./y"`
7-
* - ExportAllDeclaration — `export * from "./y"`
8-
* - ExportNamedDeclaration with — `export { x } from "./y"` / `export { x }`
9-
* declaration === null
10-
*
11-
* Anything else — `export const x = ...`, `export default ...`, function or
12-
* class declarations, expression statements — is executable logic, so the
13-
* file is not a barrel.
4+
* A Program body is barrel-shaped when every top-level statement is pure
5+
* import/re-export plumbing: ImportDeclaration, ExportAllDeclaration, or
6+
* ExportNamedDeclaration with a null declaration. Anything else (`export
7+
* const`, `export default`, function/class declarations, expression
8+
* statements) is executable logic, so the file is not a barrel.
149
*/
1510
function isReExportOnlyBody(body: EsTreeNode[]): boolean {
1611
return body.every((node) => {
@@ -25,16 +20,10 @@ function isReExportOnlyBody(body: EsTreeNode[]): boolean {
2520

2621
/**
2722
* Fallback for files whose transpiled Program body is EMPTY: ctx.ast()
28-
* transpiles TypeScript before parsing, which erases type-only syntax
29-
* (`export type { X } from "./y"`, `import type ...`), so a pure
30-
* type-re-export barrel parses to an empty Program. Conservatively inspect
31-
* the source: strip comments and blank space, then require every remaining
32-
* statement to start with `import` or `export`. A comment-only/empty file
33-
* is not a barrel.
34-
*
35-
* Handles multi-line statements (e.g. `export type {\n A,\n} from ...`)
36-
* by tracking brace depth — continuation lines inside `{ }` are part of
37-
* the enclosing import/export, not new statements.
23+
* transpiles first, erasing type-only syntax, so a pure type-re-export
24+
* barrel parses to an empty Program. Conservatively require every source
25+
* statement (comments stripped, brace depth tracked across multi-line
26+
* statements) to start with `import` or `export`; empty files are not barrels.
3827
*/
3928
function isTypeOnlyBarrel(source: string): boolean {
4029
const stripped = source

.archgate/adrs/ARCH-005-testing-standards.rules.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export default {
99
description: "Test directory structure should mirror src/ structure",
1010
severity: "error",
1111
async check(ctx) {
12-
// Get all src modules (non-index, non-cli.ts)
1312
const srcFiles = await ctx.glob("src/**/*.ts");
1413
const testFiles = await ctx.glob("tests/**/*.test.ts");
1514

.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.rules.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export default {
1010
(f) => !f.includes("tests/") && !f.includes(".archgate/")
1111
);
1212

13-
// Check for Bun.$ template literal usage
1413
const bunShellMatches = await Promise.all(
1514
files.map((file) => ctx.grep(file, /Bun\.\$`/u))
1615
);
@@ -26,7 +25,6 @@ export default {
2625
}
2726
}
2827

29-
// Check for $ import from "bun" (the shell API)
3028
const dollarImportMatches = await Promise.all(
3129
files.map((file) =>
3230
ctx.grep(file, /import\s*\{[^}]*\$[^}]*\}\s*from\s*["']bun["']/u)
@@ -44,7 +42,6 @@ export default {
4442
}
4543
}
4644

47-
// Check for await $` pattern (destructured $ usage)
4845
const destructuredMatches = await Promise.all(
4946
files.map((file) => ctx.grep(file, /await\s+\$`/u))
5047
);

.archgate/adrs/ARCH-008-typed-command-options.rules.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
/// <reference path="../rules.d.ts" />
22

33
/**
4-
* ARCH-008 enforcement, rewritten on top of ctx.ast() (ARCH-022).
5-
*
6-
* The previous implementation grepped single lines for `.option(...)` and
7-
* missed any call formatted across multiple lines — exactly the fragility
8-
* ARCH-022 was introduced to fix. These rules now walk the ESTree produced
9-
* by ctx.ast(file, "typescript") and inspect real CallExpression arguments,
10-
* so formatting, whitespace, and string escaping no longer matter.
4+
* ARCH-008 enforcement on top of ctx.ast() (ARCH-022): rules walk the ESTree
5+
* produced by ctx.ast(file, "typescript") and inspect real CallExpression
6+
* arguments, so formatting, whitespace, and string escaping do not matter —
7+
* multi-line `.option(...)` calls match the same as single-line ones.
118
*/
129

1310
/**
1411
* Description strings that enumerate a fixed set of values, e.g.
1512
* "editor integration to configure (claude, cursor, vscode, copilot)" or
1613
* "ADR domain: backend, frontend, data, architecture, general".
17-
* Same heuristic as the previous regex rule, but applied to the parsed
18-
* Literal VALUE rather than raw source text.
14+
* Matched against the parsed Literal VALUE, not raw source text.
1915
*/
2016
const CHOICE_ENUMERATION = /(?:claude.*cursor|backend.*frontend)/u;
2117

@@ -60,14 +56,11 @@ function isStringLiteral(
6056
}
6157

6258
/**
63-
* Locate the 1-based line of an option's flag string in the ORIGINAL source.
64-
*
65-
* ctx.ast(file, "typescript") parses Bun-transpiled output, which reprints
66-
* the module and collapses multi-line calls onto single lines — node.loc
67-
* therefore refers to transpiled lines and is unusable for reporting.
68-
* Searching the untranspiled source for the quoted flag literal gives an
69-
* exact line instead; when the flag can't be found (e.g. built dynamically),
70-
* the violation is reported file-only rather than with a wrong line.
59+
* Locate the 1-based line of an option's flag string in the ORIGINAL source:
60+
* ctx.ast(file, "typescript") parses Bun-transpiled output whose node.loc
61+
* refers to transpiled lines, so the untranspiled source is searched for the
62+
* quoted flag literal instead. A flag that can't be found (e.g. built
63+
* dynamically) is reported file-only rather than with a wrong line.
7164
*/
7265
function findFlagLine(source: string, flag: string): number | undefined {
7366
const needles = [`"${flag}"`, `'${flag}'`, `\`${flag}\``];

.archgate/adrs/ARCH-012-command-error-boundaries.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Async command actions that lack try-catch error boundaries produce poor user exp
1818

1919
This was discovered during a repository-wide review where `review-context`, `session-context claude-code`, and `session-context cursor` all lacked error boundaries.
2020

21+
The failure mode is also documented by incident CLI-5: `src/commands/check.ts` once wrapped only `loadRuleAdrs()` in try-catch, so a `UserError` thrown later by `runChecks()` escaped to `main().catch()`, where it was miscaptured to Sentry and exited with code 2 instead of 1. A boundary that covers only part of the action body fails exactly like no boundary at all — the try-catch MUST span the entire action.
22+
2123
ARCH-002 defines the exit code convention and logging patterns, but does not require error boundaries in command actions. This ADR complements ARCH-002 by making error boundaries mandatory.
2224

2325
**Why not a global Commander.js error handler?** Commander provides `.exitOverride()` and `.configureOutput()` for parsing errors (unknown options, missing arguments), but these do **not** cover errors thrown inside async `.action()` callbacks. Commander's `preAction`/`postAction` hooks could theoretically wrap actions, but they don't catch async errors from the action body. The `main().catch()` in `cli.ts` catches unhandled rejections as a safety net (exit 2), but per-command try-catch is needed to produce contextual error messages and exit with code 1 instead of 2.

.archgate/adrs/ARCH-012-command-error-boundaries.rules.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
/// <reference path="../rules.d.ts" />
22

33
/**
4-
* ARCH-012 enforcement, rewritten on top of ctx.ast() (ARCH-022).
5-
*
6-
* The previous implementation only regex-detected the PRESENCE of a try-catch
7-
* inside an async action. That let partial boundaries pass: src/commands/
8-
* check.ts once wrapped only loadRuleAdrs() in try/catch, and a UserError
9-
* thrown later by runChecks() escaped to main().catch(), where it was
10-
* miscaptured to Sentry with exit 2 (incident CLI-5). These rules now walk
11-
* the ESTree produced by ctx.ast(file, "typescript"): the boundary rule
12-
* additionally flags top-level awaited statements that sit OUTSIDE the
13-
* action's try block — the exact statements whose rejections escape the
14-
* boundary.
4+
* ARCH-012 enforcement on top of ctx.ast() (ARCH-022). A try-catch that
5+
* covers only part of an action is as leaky as none, so the boundary rule
6+
* walks the ESTree and flags top-level awaited statements sitting OUTSIDE
7+
* the action's try block, whose rejections escape to main().catch() and
8+
* miscapture to Sentry. See ARCH-012 for the failure it prevents.
159
*/
1610

1711
/** Node types whose bodies run in their own context — awaits inside them are
@@ -153,7 +147,6 @@ export default {
153147
"Async command actions must include try-catch error boundaries",
154148
severity: "warning",
155149
async check(ctx) {
156-
// Only check non-index command files
157150
const files = ctx.scopedFiles.filter(
158151
(f) => f.includes("commands/") && !f.endsWith("index.ts")
159152
);
@@ -235,15 +228,13 @@ export default {
235228
description:
236229
"Catch blocks in async command actions must re-throw ExitPromptError for proper Ctrl+C handling (exit 130)",
237230
async check(ctx) {
238-
// Only check non-index command files that have async actions
239231
const files = ctx.scopedFiles.filter(
240232
(f) => f.includes("commands/") && !f.endsWith("index.ts")
241233
);
242234

243235
const checks = files.map(async (file) => {
244236
const content = await ctx.readFile(file);
245237

246-
// Only check files with async actions that have try-catch
247238
const hasAsyncActionWithTryCatch =
248239
/\.action\(\s*async\s[\s\S]*?\btry\s*\{/u.test(content);
249240
if (!hasAsyncActionWithTryCatch) return;

.archgate/adrs/ARCH-015-cli-command-documentation-coverage.rules.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@ export default {
1616
"Every top-level CLI command (src/commands/<name>.ts or src/commands/<name>/index.ts) must have a corresponding reference page at docs/src/content/docs/reference/cli/<name>.mdx, and vice versa",
1717
severity: "error",
1818
async check(ctx) {
19-
// Discover top-level command names from src/commands/.
20-
// Per ARCH-001, top-level commands live at either
21-
// src/commands/<name>.ts — single-file command
22-
// src/commands/<name>/index.ts — command group
23-
// Nested files like src/commands/<name>/create.ts or
24-
// src/commands/<name>/<sub>/index.ts are subcommands and NOT
25-
// independent top-level commands.
19+
// Discover top-level command names from src/commands/. Per ARCH-001
20+
// they live at src/commands/<name>.ts (single-file) or
21+
// src/commands/<name>/index.ts (command group); nested files are
22+
// subcommands, not independent top-level commands.
2623
const commandNames = new Set<string>();
2724

2825
const topLevelFiles = await ctx.glob(`${COMMANDS_DIR}/*.ts`);
@@ -38,7 +35,6 @@ export default {
3835
commandNames.add(name);
3936
}
4037

41-
// Collect docs stems.
4238
const docFiles = await ctx.glob(`${DOCS_DIR}/*.mdx`);
4339
const docStems = new Set<string>();
4440
for (const file of docFiles) {

.archgate/adrs/ARCH-016-cli-subcommand-documentation-accuracy.rules.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,18 @@ export default {
1111
severity: "error",
1212
async check(ctx) {
1313
// ── 1. Discover subcommand names from src/commands/ ──────────────
14-
//
15-
// Top-level command groups live at src/commands/<parent>/index.ts.
16-
// Direct subcommands are either:
17-
// src/commands/<parent>/<sub>.ts (single-file subcommand)
18-
// src/commands/<parent>/<sub>/index.ts (nested command group)
19-
//
20-
// We only look one level deep: <parent>/<sub>. Files like
21-
// src/commands/adr/domain/add.ts are sub-subcommands of "adr domain"
22-
// and are NOT checked by this rule (they are documented in the
23-
// "adr domain" section as a table, not as separate headings).
24-
25-
// Find all parent command groups (dirs with an index.ts).
14+
// Direct subcommands of a group (src/commands/<parent>/index.ts) are
15+
// <parent>/<sub>.ts or <parent>/<sub>/index.ts. Only one level deep
16+
// is checked: deeper files like adr/domain/add.ts are sub-subcommands
17+
// documented as a table in the parent's section, not as headings.
18+
2619
const groupIndexFiles = await ctx.glob(`${COMMANDS_DIR}/*/index.ts`);
2720

28-
// Extract parent names from index files.
2921
const parentNames = groupIndexFiles.map((indexFile) => {
3022
const rel = indexFile.slice(COMMANDS_DIR.length + 1);
3123
return rel.split("/")[0];
3224
});
3325

34-
// Discover subcommands for all parents in parallel.
3526
const subResults = await Promise.all(
3627
parentNames.map(async (parentName) => {
3728
const [subFiles, nestedGroupFiles] = await Promise.all([
@@ -41,7 +32,6 @@ export default {
4132

4233
const subs = new Set<string>();
4334

44-
// Single-file subcommands
4535
for (const sf of subFiles) {
4636
const fileName = sf.slice(
4737
`${COMMANDS_DIR}/${parentName}/`.length
@@ -50,7 +40,6 @@ export default {
5040
subs.add(fileName.slice(0, -".ts".length));
5141
}
5242

53-
// Nested command groups
5443
for (const ngf of nestedGroupFiles) {
5544
const nestedRel = ngf.slice(
5645
`${COMMANDS_DIR}/${parentName}/`.length
@@ -74,7 +63,6 @@ export default {
7463
// ### archgate adr domain
7564
const headingPattern = /^#{1,4}\s+.*archgate\s+(\S+)\s+(\S+)/giu;
7665

77-
// Read all docs files in parallel.
7866
const docsResults = await Promise.all(
7967
[...subcommandsByParent.entries()].map(
8068
async ([parentName, subNames]) => {
@@ -91,7 +79,6 @@ export default {
9179
)
9280
);
9381

94-
// Report violations.
9582
for (const {
9683
parentName,
9784
subNames,
@@ -100,7 +87,6 @@ export default {
10087
} of docsResults) {
10188
if (docsContent === null) continue;
10289

103-
// Extract documented subcommand names from headings
10490
const documentedSubs = new Set<string>();
10591
let match;
10692
headingPattern.lastIndex = 0;

.archgate/adrs/ARCH-019-inquirer-prompt-fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,4 @@ Code reviewers MUST verify any new interactive flow wraps its prompts in `withPr
7474

7575
- [ARCH-018: Lazy-Load Heavy Dependencies](./ARCH-018-lazy-load-heavy-dependencies.md)`inquirer` is loaded lazily; this ADR governs how its prompts are invoked
7676
- [`src/helpers/prompt.ts`](../../src/helpers/prompt.ts) — defines `withPromptFix()` and the stdout patch
77+
- [Inquirer.js issue #2123](https://github.com/SBoudrias/Inquirer.js/issues/2123) — upstream report of the unrestored Windows console-mode flag

0 commit comments

Comments
 (0)