From a669cd97064b9258e752f5ea0f317dee6611e83c Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 11 May 2026 00:56:03 +0200 Subject: [PATCH 1/5] feat: support custom ADR directory via config.json paths field Allow projects to store ADRs in a directory other than the default `.archgate/adrs/` by adding a `paths` section to `.archgate/config.json`. This supports monorepo setups where ADRs live alongside other docs (e.g., `docs/adrs/`). - Add `PathsConfigSchema` with relative-path validation (no absolute paths, no `..` traversal) to `ProjectConfigSchema` - Add `resolvedProjectPaths()` in project-config.ts that layers config-aware overrides on top of the default paths - Update engine loader, ADR commands, rules shim, and install-info to use resolved paths instead of hardcoded defaults - Broaden `findProjectRoot()` to detect `.archgate/lint/` as a project marker (covers projects that moved ADRs out of `.archgate/adrs/`) - Write `rules.d.ts` to both `.archgate/` and the custom ADR directory parent so triple-slash references resolve correctly - Add Configuration reference page (EN + PT-BR) with full schema docs, path validation rules, and monorepo example - Update 7 existing doc pages (EN + PT-BR) with cross-links to the new configuration reference Signed-off-by: Rhuan Barreto --- docs/astro.config.mjs | 1 + docs/src/content/docs/concepts/adrs.mdx | 6 +- docs/src/content/docs/concepts/domains.mdx | 2 +- .../docs/getting-started/quick-start.mdx | 2 +- docs/src/content/docs/guides/writing-adrs.mdx | 2 +- .../src/content/docs/guides/writing-rules.mdx | 4 + docs/src/content/docs/pt-br/concepts/adrs.mdx | 6 +- .../content/docs/pt-br/concepts/domains.mdx | 2 +- .../pt-br/getting-started/quick-start.mdx | 2 +- .../docs/pt-br/guides/writing-adrs.mdx | 2 +- .../docs/pt-br/guides/writing-rules.mdx | 4 + .../docs/pt-br/reference/adr-schema.mdx | 6 +- .../content/docs/pt-br/reference/cli/init.mdx | 4 + .../docs/pt-br/reference/configuration.mdx | 93 +++++++++++++++++++ .../src/content/docs/reference/adr-schema.mdx | 6 +- docs/src/content/docs/reference/cli/init.mdx | 4 + .../content/docs/reference/configuration.mdx | 93 +++++++++++++++++++ src/commands/adr/create.ts | 5 +- src/commands/adr/list.ts | 5 +- src/commands/adr/show.ts | 5 +- src/commands/adr/update.ts | 9 +- src/engine/loader.ts | 11 ++- src/formats/project-config.ts | 21 +++++ src/helpers/install-info.ts | 14 ++- src/helpers/paths.ts | 16 +++- src/helpers/project-config.ts | 31 +++++++ src/helpers/rules-shim.ts | 43 +++++++-- tests/engine/loader.test.ts | 49 +++++++++- tests/formats/project-config-fuzz.test.ts | 92 ++++++++++++++++++ tests/helpers/paths.test.ts | 24 ++++- tests/helpers/project-config.test.ts | 62 +++++++++++++ 31 files changed, 586 insertions(+), 40 deletions(-) create mode 100644 docs/src/content/docs/pt-br/reference/configuration.mdx create mode 100644 docs/src/content/docs/reference/configuration.mdx diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 8cb9edeb..72f40479 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -253,6 +253,7 @@ export default defineConfig({ }, { label: "Rule API", slug: "reference/rule-api" }, { label: "ADR Schema", slug: "reference/adr-schema" }, + { label: "Configuration", slug: "reference/configuration" }, { label: "Telemetry", slug: "reference/telemetry" }, { label: "Privacy Policy", slug: "reference/privacy-policy" }, ], diff --git a/docs/src/content/docs/concepts/adrs.mdx b/docs/src/content/docs/concepts/adrs.mdx index 73be5c60..c4b18e3a 100644 --- a/docs/src/content/docs/concepts/adrs.mdx +++ b/docs/src/content/docs/concepts/adrs.mdx @@ -11,7 +11,11 @@ Archgate builds on the ADR concept by giving each decision two expressions: a ** ### ADR as Document -The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/`. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. +The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/` by default. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. + +:::tip[Custom ADR directory] +ADRs can be stored in a different directory (e.g., `docs/adrs/`) by configuring the `paths.adrs` field in `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. +::: Both humans and AI agents consume this document. When an AI coding agent is about to write code, it reads the relevant ADRs to understand the constraints before generating anything. diff --git a/docs/src/content/docs/concepts/domains.mdx b/docs/src/content/docs/concepts/domains.mdx index 183cd4ba..8154f478 100644 --- a/docs/src/content/docs/concepts/domains.mdx +++ b/docs/src/content/docs/concepts/domains.mdx @@ -110,7 +110,7 @@ archgate adr domain add security SEC archgate adr domain remove security ``` -Custom domain → prefix mappings persist in `.archgate/config.json` and are merged with the built-ins at read time. A registered custom domain behaves exactly like a built-in: `archgate adr create --domain security` auto-generates IDs like `SEC-001`, and `archgate adr list --domain security` filters to those ADRs. +Custom domain-to-prefix mappings persist in [`.archgate/config.json`](/reference/configuration/) and are merged with the built-ins at read time. A registered custom domain behaves exactly like a built-in: `archgate adr create --domain security` auto-generates IDs like `SEC-001`, and `archgate adr list --domain security` filters to those ADRs. ### Naming rules diff --git a/docs/src/content/docs/getting-started/quick-start.mdx b/docs/src/content/docs/getting-started/quick-start.mdx index 7da41c0b..8a588342 100644 --- a/docs/src/content/docs/getting-started/quick-start.mdx +++ b/docs/src/content/docs/getting-started/quick-start.mdx @@ -37,7 +37,7 @@ This creates the `.archgate/` directory with the following structure: archgate.config.ts # Archgate configuration ``` -The generated files give you a working example to build on. +The generated files give you a working example to build on. The default ADR directory is `.archgate/adrs/`, but it can be [customized](/reference/configuration/#custom-adr-directory) after initialization. ## 3. Edit the example ADR diff --git a/docs/src/content/docs/guides/writing-adrs.mdx b/docs/src/content/docs/guides/writing-adrs.mdx index 3c713d63..8cb340c4 100644 --- a/docs/src/content/docs/guides/writing-adrs.mdx +++ b/docs/src/content/docs/guides/writing-adrs.mdx @@ -21,7 +21,7 @@ You will be prompted for: 2. **Title** -- a short, descriptive name for the decision 3. **File patterns** -- optional comma-separated globs that scope rule checking (e.g., `src/commands/**/*.ts`) -The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/`. +The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/` (or a [custom directory](/reference/configuration/#custom-adr-directory) if configured). ### Non-interactive mode diff --git a/docs/src/content/docs/guides/writing-rules.mdx b/docs/src/content/docs/guides/writing-rules.mdx index 25d5875f..1c5cd285 100644 --- a/docs/src/content/docs/guides/writing-rules.mdx +++ b/docs/src/content/docs/guides/writing-rules.mdx @@ -11,6 +11,10 @@ Rules are TypeScript functions that check your codebase for ADR compliance. They ARCH-001-command-structure.rules.ts # The automated checks ``` +:::note +Rules files always live next to their companion ADR files. If you configure a [custom ADR directory](/reference/configuration/#custom-adr-directory), place rules files there too. +::: + ## Basic setup Every rules file exports a default plain object typed with `satisfies RuleSet`. Each key in the `rules` object becomes a rule ID, and each rule has a `description` and an async `check` function that receives a context object. diff --git a/docs/src/content/docs/pt-br/concepts/adrs.mdx b/docs/src/content/docs/pt-br/concepts/adrs.mdx index 6044d5b5..5beb2146 100644 --- a/docs/src/content/docs/pt-br/concepts/adrs.mdx +++ b/docs/src/content/docs/pt-br/concepts/adrs.mdx @@ -11,7 +11,11 @@ O Archgate expande o conceito de ADR dando a cada decisão duas expressões: um ### ADR como Documento -O documento é um arquivo Markdown com frontmatter YAML armazenado em `.archgate/adrs/`. Ele descreve a decisão em linguagem simples: qual problema resolve, quais alternativas foram consideradas, o que a equipe decidiu, e quais consequências seguem. +O documento é um arquivo Markdown com frontmatter YAML armazenado em `.archgate/adrs/` por padrão. Ele descreve a decisão em linguagem simples: qual problema resolve, quais alternativas foram consideradas, o que a equipe decidiu, e quais consequências seguem. + +:::tip[Diretório personalizado de ADRs] +ADRs podem ser armazenados em um diretório diferente (ex: `docs/adrs/`) configurando o campo `paths.adrs` em `.archgate/config.json`. Veja a [referência de Configuração](/reference/configuration/#diretorio-personalizado-de-adrs) para detalhes. +::: Tanto humanos quanto agentes de IA consomem esse documento. Quando um agente de IA de codificação está prestes a escrever código, ele lê os ADRs relevantes para entender as restrições antes de gerar qualquer coisa. diff --git a/docs/src/content/docs/pt-br/concepts/domains.mdx b/docs/src/content/docs/pt-br/concepts/domains.mdx index fe693f0f..210a074a 100644 --- a/docs/src/content/docs/pt-br/concepts/domains.mdx +++ b/docs/src/content/docs/pt-br/concepts/domains.mdx @@ -110,7 +110,7 @@ archgate adr domain add security SEC archgate adr domain remove security ``` -Os mapeamentos de domínio personalizado → prefixo persistem em `.archgate/config.json` e são mesclados com os integrados no momento da leitura. Um domínio personalizado registrado se comporta exatamente como um integrado: `archgate adr create --domain security` gera IDs como `SEC-001`, e `archgate adr list --domain security` filtra esses ADRs. +Os mapeamentos de domínio personalizado para prefixo persistem em [`.archgate/config.json`](/reference/configuration/) e são mesclados com os integrados no momento da leitura. Um domínio personalizado registrado se comporta exatamente como um integrado: `archgate adr create --domain security` gera IDs como `SEC-001`, e `archgate adr list --domain security` filtra esses ADRs. ### Regras de nomenclatura diff --git a/docs/src/content/docs/pt-br/getting-started/quick-start.mdx b/docs/src/content/docs/pt-br/getting-started/quick-start.mdx index 76a47720..5b185fa3 100644 --- a/docs/src/content/docs/pt-br/getting-started/quick-start.mdx +++ b/docs/src/content/docs/pt-br/getting-started/quick-start.mdx @@ -37,7 +37,7 @@ Isso cria o diretório `.archgate/` com a seguinte estrutura: archgate.config.ts # Archgate configuration ``` -Os arquivos gerados fornecem um exemplo funcional para você construir a partir dele. +Os arquivos gerados fornecem um exemplo funcional para você construir a partir dele. O diretório padrão de ADRs é `.archgate/adrs/`, mas pode ser [personalizado](/reference/configuration/#diretorio-personalizado-de-adrs) após a inicialização. ## 3. Editar o ADR de exemplo diff --git a/docs/src/content/docs/pt-br/guides/writing-adrs.mdx b/docs/src/content/docs/pt-br/guides/writing-adrs.mdx index 622c83d9..5a297576 100644 --- a/docs/src/content/docs/pt-br/guides/writing-adrs.mdx +++ b/docs/src/content/docs/pt-br/guides/writing-adrs.mdx @@ -21,7 +21,7 @@ Você será solicitado a fornecer: 2. **Title** -- um nome curto e descritivo para a decisão 3. **File patterns** -- globs opcionais separados por vírgula que delimitam o escopo da verificação de regras (ex.: `src/commands/**/*.ts`) -O CLI atribui um ID sequencial baseado no prefixo do domínio (`ARCH-001`, `FE-002`, `BE-003`, etc.) e grava o arquivo em `.archgate/adrs/`. +O CLI atribui um ID sequencial baseado no prefixo do domínio (`ARCH-001`, `FE-002`, `BE-003`, etc.) e grava o arquivo em `.archgate/adrs/` (ou em um [diretório personalizado](/reference/configuration/#diretorio-personalizado-de-adrs) se configurado). ### Modo não-interativo diff --git a/docs/src/content/docs/pt-br/guides/writing-rules.mdx b/docs/src/content/docs/pt-br/guides/writing-rules.mdx index 657d7e89..362d3366 100644 --- a/docs/src/content/docs/pt-br/guides/writing-rules.mdx +++ b/docs/src/content/docs/pt-br/guides/writing-rules.mdx @@ -11,6 +11,10 @@ Regras são funções TypeScript que verificam seu codebase quanto à conformida ARCH-001-command-structure.rules.ts # The automated checks ``` +:::note +Arquivos de regras sempre ficam ao lado de seus arquivos ADR complementares. Se voce configurar um [diretório personalizado de ADRs](/reference/configuration/#diretorio-personalizado-de-adrs), coloque os arquivos de regras lá também. +::: + ## Configuração básica Todo arquivo de regras exporta por padrão um objeto simples tipado com `satisfies RuleSet`. Cada chave no objeto `rules` se torna um ID de regra, e cada regra tem uma `description` e uma função async `check` que recebe um objeto de contexto. diff --git a/docs/src/content/docs/pt-br/reference/adr-schema.mdx b/docs/src/content/docs/pt-br/reference/adr-schema.mdx index 3003460b..e52e55e9 100644 --- a/docs/src/content/docs/pt-br/reference/adr-schema.mdx +++ b/docs/src/content/docs/pt-br/reference/adr-schema.mdx @@ -3,7 +3,11 @@ title: Esquema de ADR description: Referência completa do esquema YAML frontmatter e estrutura markdown para Architecture Decision Records do Archgate. Todos os campos e regras de validação. --- -Todo ADR do Archgate é um arquivo Markdown armazenado em `.archgate/adrs/` com frontmatter YAML que define a identidade e o escopo da decisão. Esta página documenta o esquema do frontmatter, a estrutura das seções markdown e o comportamento de validação. +Todo ADR do Archgate é um arquivo Markdown armazenado em `.archgate/adrs/` (por padrão) com frontmatter YAML que define a identidade e o escopo da decisão. Esta página documenta o esquema do frontmatter, a estrutura das seções markdown e o comportamento de validação. + +:::note +O diretório de ADRs pode ser personalizado via `.archgate/config.json`. Veja a [referência de Configuração](/reference/configuration/#diretorio-personalizado-de-adrs) para detalhes. +::: ## Esquema do Frontmatter diff --git a/docs/src/content/docs/pt-br/reference/cli/init.mdx b/docs/src/content/docs/pt-br/reference/cli/init.mdx index d20b562d..2fef472b 100644 --- a/docs/src/content/docs/pt-br/reference/cli/init.mdx +++ b/docs/src/content/docs/pt-br/reference/cli/init.mdx @@ -51,3 +51,7 @@ Quando `--editor cursor` é usado, a saída mostra `.cursor/` em vez de `.claude lint/ archgate.config.ts # Archgate configuration ``` + +:::tip +Após a inicialização, você pode configurar um diretório personalizado para ADRs editando `.archgate/config.json`. Veja a [referência de Configuração](/reference/configuration/#diretorio-personalizado-de-adrs) para detalhes. +::: diff --git a/docs/src/content/docs/pt-br/reference/configuration.mdx b/docs/src/content/docs/pt-br/reference/configuration.mdx new file mode 100644 index 00000000..492aa6c1 --- /dev/null +++ b/docs/src/content/docs/pt-br/reference/configuration.mdx @@ -0,0 +1,93 @@ +--- +title: Configuração +description: Referência do arquivo de configuração .archgate/config.json. Configure diretórios personalizados para ADRs, mapeamentos de domínios e configurações do projeto. +--- + +O arquivo `.archgate/config.json` armazena configurações do projeto que são versionadas no controle de versão e compartilhadas com toda a equipe. + +Este arquivo é criado automaticamente pelo `archgate init` (quando domínios customizados são registrados) ou quando você adiciona configurações manualmente. Ele fica dentro do diretório `.archgate/` na raiz do seu projeto. + +## Schema + +```json +{ + "domains": { "security": "SEC", "compliance": "COMP" }, + "paths": { "adrs": "docs/adrs", "rules": "docs/adrs" } +} +``` + +### `domains` + +Mapeamentos personalizados de domínio para prefixo. Veja [Domínios Personalizados](/concepts/domains/#custom-domains) para detalhes. + +| Chave | Tipo | Descrição | +| ------ | -------- | ------------------------------------------------------------------------------------------------------- | +| _nome_ | `string` | Nome do domínio (kebab-case minúsculo, 2-32 chars) mapeia para um prefixo de ID (maiúsculo, 2-10 chars) | + +Esses são mesclados com os domínios built-in (`backend`, `frontend`, `data`, `architecture`, `general`) em tempo de leitura. Entradas personalizadas não podem sobrescrever nomes ou prefixos built-in. + +### `paths` + +Sobrescreve os diretórios padrão para ADRs e regras. + +| Campo | Tipo | Padrão | Descrição | +| ------- | -------- | ---------------- | ------------------------------------------------ | +| `adrs` | `string` | `.archgate/adrs` | Caminho relativo para o diretório de ADRs | +| `rules` | `string` | `.archgate/lint` | Caminho relativo para o diretório de regras/lint | + +Ambos os campos são opcionais. Quando omitidos, os diretórios padrão `.archgate/adrs/` e `.archgate/lint/` são usados. + +#### Validação de caminhos + +- Caminhos **devem ser relativos** à raiz do projeto -- caminhos absolutos (ex: `/docs/adrs`, `C:\docs\adrs`) são rejeitados. +- Caminhos **não devem conter segmentos `..`** -- travessia acima da raiz do projeto não é permitida (ex: `../other-repo/adrs` é rejeitado). +- Caminhos usam barras (`/`) como separadores, seguindo as convenções padrão de glob. + +## Diretório personalizado de ADRs + +Por padrão, ADRs ficam em `.archgate/adrs/`. Para armazená-los em um diretório diferente (ex: `docs/adrs/`), adicione uma seção `paths` ao `.archgate/config.json`: + +```json +{ "paths": { "adrs": "docs/adrs" } } +``` + +Após adicionar a configuração: + +1. Crie o diretório de destino (ex: `mkdir -p docs/adrs`) +2. Mova os arquivos ADR existentes e seus arquivos `.rules.ts` complementares de `.archgate/adrs/` para o novo diretório +3. Execute `archgate check` para verificar se as regras ainda carregam corretamente + +Todos os comandos do CLI (`archgate adr list`, `archgate adr create`, `archgate check`, `archgate review-context`) leem automaticamente o diretório configurado. + +:::caution +O diretório `.archgate/` deve continuar existindo -- ele é o marcador de projeto usado pelo CLI para localizar a raiz do seu projeto. Não o exclua após configurar caminhos personalizados. +::: + +### Exemplo: pasta de documentação em monorepo + +Um padrão comum é colocar ADRs junto com outra documentação: + +``` +my-project/ + .archgate/ + config.json # { "paths": { "adrs": "docs/adrs" } } + lint/ + rules.d.ts + docs/ + adrs/ + ARCH-001-api-design.md + ARCH-001-api-design.rules.ts + BE-001-database-access.md + BE-001-database-access.rules.ts + rules.d.ts # gerado automaticamente pelo archgate check + guides/ + ... + src/ + ... +``` + +## Notas + +- A configuração `paths` é uma **definição de equipe** -- é versionada no controle de versão e se aplica a todos os membros da equipe. Não há override de nível de usuário para caminhos de ADR. +- Alterar a configuração requer editar manualmente `.archgate/config.json` após executar `archgate init`. +- O arquivo de definições de tipo `rules.d.ts` é escrito automaticamente tanto em `.archgate/` quanto no diretório pai do diretório de ADR configurado, para que os arquivos `.rules.ts` complementares resolvam corretamente sua diretiva `/// `. diff --git a/docs/src/content/docs/reference/adr-schema.mdx b/docs/src/content/docs/reference/adr-schema.mdx index 2a7d2243..966e373b 100644 --- a/docs/src/content/docs/reference/adr-schema.mdx +++ b/docs/src/content/docs/reference/adr-schema.mdx @@ -3,7 +3,11 @@ title: ADR Schema description: Complete YAML frontmatter schema and markdown structure reference for Archgate Architecture Decision Records. All fields and validation rules explained. --- -Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. +Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` (by default) with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. + +:::note +The ADR directory can be customized via `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. +::: ## Frontmatter Schema diff --git a/docs/src/content/docs/reference/cli/init.mdx b/docs/src/content/docs/reference/cli/init.mdx index 46a4de4c..0f33b8b1 100644 --- a/docs/src/content/docs/reference/cli/init.mdx +++ b/docs/src/content/docs/reference/cli/init.mdx @@ -51,3 +51,7 @@ When `--editor cursor` is used, the output shows `.cursor/` instead of `.claude/ lint/ archgate.config.ts # Archgate configuration ``` + +:::tip +After initialization, you can configure a custom directory for ADRs by editing `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. +::: diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx new file mode 100644 index 00000000..0d5803bf --- /dev/null +++ b/docs/src/content/docs/reference/configuration.mdx @@ -0,0 +1,93 @@ +--- +title: Configuration +description: Reference for the .archgate/config.json project configuration file. Configure custom ADR directories, domain mappings, and project-level settings. +--- + +The `.archgate/config.json` file stores project-level configuration that is committed to version control and shared across the team. + +This file is created automatically by `archgate init` (when custom domains are registered) or when you manually add configuration. It lives inside the `.archgate/` directory at your project root. + +## Schema + +```json +{ + "domains": { "security": "SEC", "compliance": "COMP" }, + "paths": { "adrs": "docs/adrs", "rules": "docs/adrs" } +} +``` + +### `domains` + +Custom domain-to-prefix mappings. See [Custom Domains](/concepts/domains/#custom-domains) for details. + +| Key | Type | Description | +| ------ | -------- | ------------------------------------------------------------------------------------------- | +| _name_ | `string` | Domain name (lowercase kebab-case, 2-32 chars) maps to an ID prefix (uppercase, 2-10 chars) | + +These are merged with the built-in domains (`backend`, `frontend`, `data`, `architecture`, `general`) at read time. Custom entries cannot override built-in names or prefixes. + +### `paths` + +Override default directories for ADRs and rules. + +| Field | Type | Default | Description | +| ------- | -------- | ---------------- | ----------------------------------------- | +| `adrs` | `string` | `.archgate/adrs` | Relative path to the ADR directory | +| `rules` | `string` | `.archgate/lint` | Relative path to the rules/lint directory | + +Both fields are optional. When omitted, the default `.archgate/adrs/` and `.archgate/lint/` directories are used. + +#### Path validation + +- Paths **must be relative** to the project root -- absolute paths (e.g., `/docs/adrs`, `C:\docs\adrs`) are rejected. +- Paths **must not contain `..` segments** -- traversal above the project root is not allowed (e.g., `../other-repo/adrs` is rejected). +- Paths use forward slashes (`/`) as separators, matching standard glob conventions. + +## Custom ADR directory + +By default, ADRs live in `.archgate/adrs/`. To store them in a different directory (e.g., `docs/adrs/`), add a `paths` section to `.archgate/config.json`: + +```json +{ "paths": { "adrs": "docs/adrs" } } +``` + +After adding the configuration: + +1. Create the target directory (e.g., `mkdir -p docs/adrs`) +2. Move existing ADR files and their companion `.rules.ts` files from `.archgate/adrs/` to the new directory +3. Run `archgate check` to verify the rules still load correctly + +All CLI commands (`archgate adr list`, `archgate adr create`, `archgate check`, `archgate review-context`) automatically read the configured directory. + +:::caution +The `.archgate/` directory must still exist -- it is the project marker used by the CLI to locate your project root. Do not delete it after configuring custom paths. +::: + +### Example: monorepo documentation folder + +A common pattern is placing ADRs alongside other documentation: + +``` +my-project/ + .archgate/ + config.json # { "paths": { "adrs": "docs/adrs" } } + lint/ + rules.d.ts + docs/ + adrs/ + ARCH-001-api-design.md + ARCH-001-api-design.rules.ts + BE-001-database-access.md + BE-001-database-access.rules.ts + rules.d.ts # auto-generated by archgate check + guides/ + ... + src/ + ... +``` + +## Notes + +- The `paths` configuration is a **team-wide setting** -- it is committed to version control and applies to all team members. There is no user-level override for ADR paths. +- Changing the configuration requires manually editing `.archgate/config.json` after running `archgate init`. +- The `rules.d.ts` type definitions file is automatically written to both `.archgate/` and the parent of the configured ADR directory, so companion `.rules.ts` files resolve their `/// ` directive correctly. diff --git a/src/commands/adr/create.ts b/src/commands/adr/create.ts index 5547a3d4..a60755d6 100644 --- a/src/commands/adr/create.ts +++ b/src/commands/adr/create.ts @@ -9,10 +9,11 @@ import { createAdrFile } from "../../helpers/adr-writer"; import { exitWith } from "../../helpers/exit"; import { logError } from "../../helpers/log"; import { formatJSON, isAgentContext } from "../../helpers/output"; -import { findProjectRoot, projectPaths } from "../../helpers/paths"; +import { findProjectRoot } from "../../helpers/paths"; import { getAllDomainNames, resolveDomainPrefix, + resolvedProjectPaths, } from "../../helpers/project-config"; export function registerAdrCreateCommand(adr: Command) { @@ -37,7 +38,7 @@ export function registerAdrCreateCommand(adr: Command) { } try { - const paths = projectPaths(projectRoot); + const paths = resolvedProjectPaths(projectRoot); let domain: AdrDomain; let title: string; diff --git a/src/commands/adr/list.ts b/src/commands/adr/list.ts index a69d9b6a..02dee029 100644 --- a/src/commands/adr/list.ts +++ b/src/commands/adr/list.ts @@ -9,7 +9,8 @@ import { parseAllAdrs } from "../../engine/loader"; import { exitWith } from "../../helpers/exit"; import { logError } from "../../helpers/log"; import { formatJSON, isAgentContext } from "../../helpers/output"; -import { findProjectRoot, projectPaths } from "../../helpers/paths"; +import { findProjectRoot } from "../../helpers/paths"; +import { resolvedProjectPaths } from "../../helpers/project-config"; export function registerAdrListCommand(adr: Command) { adr @@ -26,7 +27,7 @@ export function registerAdrListCommand(adr: Command) { } try { - const paths = projectPaths(projectRoot); + const paths = resolvedProjectPaths(projectRoot); if (!existsSync(paths.adrsDir)) { console.log("No ADRs found."); diff --git a/src/commands/adr/show.ts b/src/commands/adr/show.ts index e580a569..e48c23ff 100644 --- a/src/commands/adr/show.ts +++ b/src/commands/adr/show.ts @@ -5,7 +5,8 @@ import type { Command } from "@commander-js/extra-typings"; import { findAdrFileById } from "../../helpers/adr-writer"; import { exitWith } from "../../helpers/exit"; import { logError } from "../../helpers/log"; -import { findProjectRoot, projectPaths } from "../../helpers/paths"; +import { findProjectRoot } from "../../helpers/paths"; +import { resolvedProjectPaths } from "../../helpers/project-config"; export function registerAdrShowCommand(adr: Command) { adr @@ -21,7 +22,7 @@ export function registerAdrShowCommand(adr: Command) { } try { - const { adrsDir } = projectPaths(projectRoot); + const { adrsDir } = resolvedProjectPaths(projectRoot); const adr = await findAdrFileById(adrsDir, id); if (!adr) { diff --git a/src/commands/adr/update.ts b/src/commands/adr/update.ts index 72b3dee6..8623d0cc 100644 --- a/src/commands/adr/update.ts +++ b/src/commands/adr/update.ts @@ -6,8 +6,11 @@ import { updateAdrFile } from "../../helpers/adr-writer"; import { exitWith } from "../../helpers/exit"; import { logError } from "../../helpers/log"; import { formatJSON, isAgentContext } from "../../helpers/output"; -import { findProjectRoot, projectPaths } from "../../helpers/paths"; -import { resolveDomainPrefix } from "../../helpers/project-config"; +import { findProjectRoot } from "../../helpers/paths"; +import { + resolveDomainPrefix, + resolvedProjectPaths, +} from "../../helpers/project-config"; export function registerAdrUpdateCommand(adr: Command) { adr @@ -33,7 +36,7 @@ export function registerAdrUpdateCommand(adr: Command) { await exitWith(1); return; } - const paths = projectPaths(projectRoot); + const paths = resolvedProjectPaths(projectRoot); const files = opts.files ? opts.files diff --git a/src/engine/loader.ts b/src/engine/loader.ts index db48bf89..fc7b5045 100644 --- a/src/engine/loader.ts +++ b/src/engine/loader.ts @@ -28,7 +28,7 @@ import { relative } from "node:path"; import type { ViolationDetail } from "../formats/rules"; import { logDebug } from "../helpers/log"; -import { projectPaths } from "../helpers/paths"; +import { resolvedProjectPaths } from "../helpers/project-config"; import { ensureRulesShim } from "../helpers/rules-shim"; import { scanRuleSource } from "./rule-scanner"; @@ -164,7 +164,7 @@ export function parseAllAdrs(projectRoot: string): Promise { const cached = parsedAdrsCache.get(projectRoot); if (cached) return cached; - const pp = projectPaths(projectRoot); + const pp = resolvedProjectPaths(projectRoot); const adrsDir = pp.adrsDir; const promise = (async () => { @@ -202,11 +202,12 @@ export async function loadRuleAdrs( projectRoot: string, filterAdrId?: string ): Promise { - const pp = projectPaths(projectRoot); + const pp = resolvedProjectPaths(projectRoot); // Ensure rules.d.ts exists so .rules.ts files get type checking - // without requiring node_modules (supports non-JS projects) - await ensureRulesShim(projectRoot); + // without requiring node_modules (supports non-JS projects). + // When ADRs live in a custom directory, also write the shim there. + await ensureRulesShim(projectRoot, pp.adrsDir); const adrsDir = pp.adrsDir; diff --git a/src/formats/project-config.ts b/src/formats/project-config.ts index 6ec1b3f8..cf051663 100644 --- a/src/formats/project-config.ts +++ b/src/formats/project-config.ts @@ -23,9 +23,30 @@ export const DomainPrefixSchema = z "domain prefix must be uppercase (e.g. 'SEC', 'MLOPS')" ); +/** + * Validate that a path is relative and does not escape the project root. + * Rejects absolute paths (leading `/`, `\`, or drive letters like `C:\`) + * and `..` segments that could traverse above the project root. + */ +const RelativePathSchema = z + .string() + .min(1, "path must not be empty") + .refine((p) => !/^[/\\]/u.test(p) && !/^[A-Za-z]:[/\\]/u.test(p), { + message: "path must be relative (no leading / or drive letter)", + }) + .refine((p) => !/(^|\/)\.\.($|\/)/u.test(p.replaceAll("\\", "/")), { + message: "path must not contain '..' segments", + }); + +export const PathsConfigSchema = z.object({ + adrs: RelativePathSchema.optional(), + rules: RelativePathSchema.optional(), +}); + export const ProjectConfigSchema = z .object({ domains: z.record(DomainNameSchema, DomainPrefixSchema).default({}), + paths: PathsConfigSchema.optional(), }) .default({ domains: {} }); diff --git a/src/helpers/install-info.ts b/src/helpers/install-info.ts index 541b3bd5..2d1b55fc 100644 --- a/src/helpers/install-info.ts +++ b/src/helpers/install-info.ts @@ -11,6 +11,7 @@ import { existsSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { internalPath } from "./paths"; +import { resolvedProjectPaths } from "./project-config"; // --------------------------------------------------------------------------- // Install method detection (cached) @@ -78,8 +79,9 @@ export interface ProjectContext { * cheap enough to re-run on every event, and worth it for accuracy. */ export function getProjectContext(): ProjectContext { - const adrsDir = join(process.cwd(), ".archgate", "adrs"); - const hasProject = existsSync(adrsDir); + const cwd = process.cwd(); + const archgateDir = join(cwd, ".archgate"); + const hasProject = existsSync(archgateDir); if (!hasProject) { return { @@ -90,6 +92,14 @@ export function getProjectContext(): ProjectContext { }; } + // Use resolved paths so we scan the configured ADR directory, + // not just the default `.archgate/adrs/`. + const { adrsDir } = resolvedProjectPaths(cwd); + + if (!existsSync(adrsDir)) { + return { hasProject: true, adrCount: 0, adrWithRulesCount: 0, domains: [] }; + } + try { const entries = readdirSync(adrsDir); const mdFiles = entries.filter((f) => f.endsWith(".md")); diff --git a/src/helpers/paths.ts b/src/helpers/paths.ts index 5ca65b88..93840829 100644 --- a/src/helpers/paths.ts +++ b/src/helpers/paths.ts @@ -112,15 +112,25 @@ export function createPathIfNotExists(path: string) { } /** - * Walk up from cwd to find the nearest directory containing .archgate/adrs/. - * Returns the project root path or null if not found. + * Walk up from cwd to find the nearest directory containing an archgate + * project. A directory is a project root when it has either: + * - `.archgate/adrs/` — standard project layout + * - `.archgate/lint/` — also created by `archgate init` + * + * Both directories are created by `archgate init` and are project-specific. + * We cannot match on `.archgate/` alone because `~/.archgate/` is the + * CLI's user-level cache directory (binary installs, credentials, etc.) + * and would produce false positives. We also avoid matching on + * `.archgate/config.json` because `~/.archgate/config.json` stores + * telemetry settings. */ export function findProjectRoot(startDir?: string): string | null { let dir = startDir ?? process.cwd(); while (true) { const adrsDir = join(dir, ".archgate", "adrs"); - if (existsSync(adrsDir)) { + const lintDir = join(dir, ".archgate", "lint"); + if (existsSync(adrsDir) || existsSync(lintDir)) { return dir; } diff --git a/src/helpers/project-config.ts b/src/helpers/project-config.ts index 1436a674..9e0769d2 100644 --- a/src/helpers/project-config.ts +++ b/src/helpers/project-config.ts @@ -10,6 +10,7 @@ */ import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; import { DOMAIN_PREFIXES as DEFAULT_DOMAIN_PREFIXES, @@ -202,3 +203,33 @@ export async function removeCustomDomain( await saveProjectConfig(projectRoot, next); return { config: next, removed: true }; } + +/** + * Resolve project paths with config-aware overrides. + * + * Reads `.archgate/config.json` and applies any custom `paths.adrs` or + * `paths.rules` overrides. When `paths.rules` is not set, rules are + * loaded from the same directory as ADRs (co-location convention). + * Falls back to the standard `.archgate/adrs/` and `.archgate/lint/` + * defaults when no `paths` config is present. + */ +export function resolvedProjectPaths(projectRoot: string): { + root: string; + adrsDir: string; + lintDir: string; +} { + const defaults = projectPaths(projectRoot); + const config = loadProjectConfig(projectRoot); + + if (!config.paths) return defaults; + + return { + root: defaults.root, + adrsDir: config.paths.adrs + ? join(projectRoot, config.paths.adrs) + : defaults.adrsDir, + lintDir: config.paths.rules + ? join(projectRoot, config.paths.rules) + : defaults.lintDir, + }; +} diff --git a/src/helpers/rules-shim.ts b/src/helpers/rules-shim.ts index 4a933e14..982b8446 100644 --- a/src/helpers/rules-shim.ts +++ b/src/helpers/rules-shim.ts @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2026 Archgate +import { dirname, join } from "node:path"; + import { logDebug } from "./log"; import { projectPath } from "./paths"; @@ -121,14 +123,10 @@ export async function writeRulesShim(projectRoot: string): Promise { } /** - * Ensure rules.d.ts exists and is up-to-date. Skips the disk write when the - * on-disk content already matches — `archgate check` calls this every run, - * and the content only changes when the CLI version changes. + * Write `rules.d.ts` to a specific path if it doesn't already match. + * Returns true when a disk write occurred, false when skipped (up-to-date). */ -export async function ensureRulesShim(projectRoot: string): Promise { - const dtsPath = projectPath(projectRoot, "rules.d.ts"); - const expected = generateRulesDts(); - +async function ensureShimAt(dtsPath: string, expected: string): Promise { try { const existing = await Bun.file(dtsPath).text(); if (existing === expected) { @@ -142,3 +140,34 @@ export async function ensureRulesShim(projectRoot: string): Promise { await Bun.write(dtsPath, expected); logDebug("Rules type definitions written:", dtsPath); } + +/** + * Ensure rules.d.ts exists and is up-to-date. Skips the disk write when the + * on-disk content already matches — `archgate check` calls this every run, + * and the content only changes when the CLI version changes. + * + * When `customAdrsDir` is provided and differs from the default + * `.archgate/adrs/`, a copy of `rules.d.ts` is also written to the parent + * of that directory so that companion `.rules.ts` files' triple-slash + * `/// ` resolves correctly. + */ +export async function ensureRulesShim( + projectRoot: string, + customAdrsDir?: string +): Promise { + const defaultDtsPath = projectPath(projectRoot, "rules.d.ts"); + const expected = generateRulesDts(); + + await ensureShimAt(defaultDtsPath, expected); + + // When ADRs live in a custom directory, the `.rules.ts` files use + // `/// ` which resolves relative to + // their own directory. Write a shim to the parent of the custom ADR dir. + if (customAdrsDir) { + const defaultAdrsDir = join(projectRoot, ".archgate", "adrs"); + if (customAdrsDir !== defaultAdrsDir) { + const customDtsPath = join(dirname(customAdrsDir), "rules.d.ts"); + await ensureShimAt(customDtsPath, expected); + } + } +} diff --git a/tests/engine/loader.test.ts b/tests/engine/loader.test.ts index 9f2dc3fb..8a910332 100644 --- a/tests/engine/loader.test.ts +++ b/tests/engine/loader.test.ts @@ -11,7 +11,8 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; -import { loadRuleAdrs } from "../../src/engine/loader"; +import { loadRuleAdrs, parseAllAdrs } from "../../src/engine/loader"; +import { saveProjectConfig } from "../../src/helpers/project-config"; describe("loadRuleAdrs", () => { let tempDir: string; @@ -203,4 +204,50 @@ export default { expect(blocked.value.error).toContain("2 violations"); expect(blocked.value.violations).toHaveLength(2); }); + + test("loads ADRs from custom directory configured in config.json", async () => { + // Create a custom ADR directory outside .archgate/ + const customAdrsDir = join(tempDir, "docs", "adrs"); + mkdirSync(customAdrsDir, { recursive: true }); + + // Configure the custom path + await saveProjectConfig(tempDir, { + domains: {}, + paths: { adrs: "docs/adrs" }, + }); + + // Place ADR + rules in the custom directory + copyFileSync( + join(fixturesDir, "TEST-001-sample.md"), + join(customAdrsDir, "TEST-001-sample.md") + ); + writeRulesTs(customAdrsDir, "TEST-001-sample"); + + const loaded = await loadRuleAdrs(tempDir); + expect(loaded).toHaveLength(1); + expect(loaded[0].type).toBe("loaded"); + const first = loaded[0] as Extract<(typeof loaded)[0], { type: "loaded" }>; + expect(first.value.adr.frontmatter.id).toBe("TEST-001"); + }); + + test("parseAllAdrs reads from custom directory", async () => { + // Configure custom path + const customAdrsDir = join(tempDir, "governance"); + mkdirSync(customAdrsDir, { recursive: true }); + + await saveProjectConfig(tempDir, { + domains: {}, + paths: { adrs: "governance" }, + }); + + // Place ADR in custom dir, NOT in .archgate/adrs/ + copyFileSync( + join(fixturesDir, "TEST-001-sample.md"), + join(customAdrsDir, "TEST-001-sample.md") + ); + + const parsed = await parseAllAdrs(tempDir); + expect(parsed).toHaveLength(1); + expect(parsed[0].adr.frontmatter.id).toBe("TEST-001"); + }); }); diff --git a/tests/formats/project-config-fuzz.test.ts b/tests/formats/project-config-fuzz.test.ts index 3583dfff..8346f5ed 100644 --- a/tests/formats/project-config-fuzz.test.ts +++ b/tests/formats/project-config-fuzz.test.ts @@ -7,6 +7,7 @@ import fc from "fast-check"; import { DomainNameSchema, DomainPrefixSchema, + PathsConfigSchema, ProjectConfigSchema, } from "../../src/formats/project-config"; @@ -205,4 +206,95 @@ describe("ProjectConfigSchema fuzz", () => { } } }); + + test("accepts config with valid paths", () => { + const cases = [ + { domains: {}, paths: { adrs: "docs/adrs" } }, + { domains: {}, paths: { rules: "custom/rules" } }, + { domains: {}, paths: { adrs: "docs/adrs", rules: "docs/adrs" } }, + { domains: {}, paths: {} }, + ]; + for (const c of cases) { + const result = ProjectConfigSchema.safeParse(c); + expect(result.success).toBe(true); + } + }); + + test("rejects config with absolute paths", () => { + const cases = [ + { domains: {}, paths: { adrs: "/absolute/path" } }, + { domains: {}, paths: { rules: "/etc/rules" } }, + { domains: {}, paths: { adrs: "C:\\absolute\\path" } }, + ]; + for (const c of cases) { + const result = ProjectConfigSchema.safeParse(c); + expect(result.success).toBe(false); + } + }); + + test("rejects config with '..' path segments", () => { + const cases = [ + { domains: {}, paths: { adrs: "../escape" } }, + { domains: {}, paths: { adrs: "docs/../../escape" } }, + { domains: {}, paths: { rules: ".." } }, + ]; + for (const c of cases) { + const result = ProjectConfigSchema.safeParse(c); + expect(result.success).toBe(false); + } + }); +}); + +// --------------------------------------------------------------------------- +// PathsConfigSchema +// --------------------------------------------------------------------------- + +describe("PathsConfigSchema fuzz", () => { + test("safeParse never throws on arbitrary strings for adrs/rules", () => { + fc.assert( + fc.property(fc.string({ minLength: 0, maxLength: 50 }), (input) => { + const result = PathsConfigSchema.safeParse({ + adrs: input, + rules: input, + }); + expect(result).toHaveProperty("success"); + }), + { numRuns: NUM_RUNS } + ); + }); + + test("accepts valid relative paths", () => { + const valid = [ + "docs/adrs", + "custom", + "a/b/c/d", + "src/governance/adrs", + "adrs", + ]; + for (const p of valid) { + const result = PathsConfigSchema.safeParse({ adrs: p }); + expect(result.success).toBe(true); + } + }); + + test("rejects absolute paths", () => { + const absolute = ["/root", "\\root", "C:\\path", "D:/path"]; + for (const p of absolute) { + const result = PathsConfigSchema.safeParse({ adrs: p }); + expect(result.success).toBe(false); + } + }); + + test("rejects paths with '..' segments", () => { + const escaping = ["..", "../foo", "foo/../bar", "foo/.."]; + for (const p of escaping) { + const result = PathsConfigSchema.safeParse({ adrs: p }); + expect(result.success).toBe(false); + } + }); + + test("rejects empty strings", () => { + const result = PathsConfigSchema.safeParse({ adrs: "" }); + expect(result.success).toBe(false); + }); }); diff --git a/tests/helpers/paths.test.ts b/tests/helpers/paths.test.ts index 798ba1d9..4cbbf5ae 100644 --- a/tests/helpers/paths.test.ts +++ b/tests/helpers/paths.test.ts @@ -25,6 +25,15 @@ describe("findProjectRoot", () => { expect(result).toBe(tempDir); }); + test("finds root when .archgate/lint/ exists (custom ADR paths)", () => { + // A project with custom ADR paths may not have .archgate/adrs/, + // but .archgate/lint/ is always created by `archgate init`. + mkdirSync(join(tempDir, ".archgate", "lint"), { recursive: true }); + + const result = findProjectRoot(tempDir); + expect(result).toBe(tempDir); + }); + test("finds root from a subdirectory", () => { mkdirSync(join(tempDir, ".archgate", "adrs"), { recursive: true }); const subDir = join(tempDir, "src", "commands"); @@ -34,8 +43,17 @@ describe("findProjectRoot", () => { expect(result).toBe(tempDir); }); - test("returns null when no .archgate/adrs/ found", () => { - const result = findProjectRoot(tempDir); - expect(result).toBeNull(); + test("does not match directory without .archgate/adrs/ or .archgate/lint/", () => { + // A directory with only a bare .archgate/ (like ~/.archgate/ user cache) + // should NOT be detected as a project root. + const parent = join(tempDir, "parent"); + const child = join(parent, "child"); + mkdirSync(child, { recursive: true }); + // Create bare .archgate/ with no adrs/ or lint/ + mkdirSync(join(parent, ".archgate"), { recursive: true }); + + const result = findProjectRoot(child); + // Should walk past parent (bare .archgate/) without matching it + expect(result).not.toBe(parent); }); }); diff --git a/tests/helpers/project-config.test.ts b/tests/helpers/project-config.test.ts index 056e9983..8ccfa2ca 100644 --- a/tests/helpers/project-config.test.ts +++ b/tests/helpers/project-config.test.ts @@ -14,6 +14,7 @@ import { loadProjectConfig, removeCustomDomain, resolveDomainPrefix, + resolvedProjectPaths, saveProjectConfig, } from "../../src/helpers/project-config"; @@ -135,4 +136,65 @@ describe("project-config", () => { await saveProjectConfig(projectRoot, { domains: { infra: "INFRA" } }); expect(loadProjectConfig(projectRoot).domains.infra).toBe("INFRA"); }); + + // ------------------------------------------------------------------------- + // resolvedProjectPaths + // ------------------------------------------------------------------------- + + describe("resolvedProjectPaths", () => { + test("returns defaults when no paths config is set", () => { + const paths = resolvedProjectPaths(projectRoot); + expect(paths.adrsDir).toBe(join(projectRoot, ".archgate", "adrs")); + expect(paths.lintDir).toBe(join(projectRoot, ".archgate", "lint")); + }); + + test("overrides adrsDir when paths.adrs is configured", async () => { + await saveProjectConfig(projectRoot, { + domains: {}, + paths: { adrs: "docs/adrs" }, + }); + const paths = resolvedProjectPaths(projectRoot); + expect(paths.adrsDir).toBe(join(projectRoot, "docs", "adrs")); + // lintDir stays default when paths.rules is not set + expect(paths.lintDir).toBe(join(projectRoot, ".archgate", "lint")); + }); + + test("overrides lintDir when paths.rules is configured", async () => { + await saveProjectConfig(projectRoot, { + domains: {}, + paths: { rules: "docs/rules" }, + }); + const paths = resolvedProjectPaths(projectRoot); + // adrsDir stays default when paths.adrs is not set + expect(paths.adrsDir).toBe(join(projectRoot, ".archgate", "adrs")); + expect(paths.lintDir).toBe(join(projectRoot, "docs", "rules")); + }); + + test("overrides both when paths.adrs and paths.rules are configured", async () => { + await saveProjectConfig(projectRoot, { + domains: {}, + paths: { adrs: "docs/adrs", rules: "docs/rules" }, + }); + const paths = resolvedProjectPaths(projectRoot); + expect(paths.adrsDir).toBe(join(projectRoot, "docs", "adrs")); + expect(paths.lintDir).toBe(join(projectRoot, "docs", "rules")); + }); + + test("root always points to .archgate/ regardless of paths config", async () => { + await saveProjectConfig(projectRoot, { + domains: {}, + paths: { adrs: "custom/adrs" }, + }); + const paths = resolvedProjectPaths(projectRoot); + expect(paths.root).toBe(join(projectRoot, ".archgate")); + }); + + test("ignores invalid config and falls back to defaults", async () => { + // Write a malformed config file + const configPath = join(projectRoot, ".archgate", "config.json"); + await Bun.write(configPath, "not valid json"); + const paths = resolvedProjectPaths(projectRoot); + expect(paths.adrsDir).toBe(join(projectRoot, ".archgate", "adrs")); + }); + }); }); From 0c1c63eeb3c9a31b5cd2b8c5f696702c64c0e94c Mon Sep 17 00:00:00 2001 From: rhuanbarreto <283004+rhuanbarreto@users.noreply.github.com> Date: Sun, 10 May 2026 22:57:18 +0000 Subject: [PATCH 2/5] docs: regenerate llms-full.txt Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/public/llms-full.txt | 115 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 5 deletions(-) diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index c8e004bd..e36225c5 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -307,7 +307,7 @@ This creates the `.archgate/` directory with the following structure: archgate.config.ts # Archgate configuration ``` -The generated files give you a working example to build on. +The generated files give you a working example to build on. The default ADR directory is `.archgate/adrs/`, but it can be [customized](/reference/configuration/#custom-adr-directory) after initialization. ## 3. Edit the example ADR @@ -427,7 +427,9 @@ Archgate builds on the ADR concept by giving each decision two expressions: a ** ### ADR as Document -The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/`. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. +The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/` by default. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. + +ADRs can be stored in a different directory (e.g., `docs/adrs/`) by configuring the `paths.adrs` field in `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. Both humans and AI agents consume this document. When an AI coding agent is about to write code, it reads the relevant ADRs to understand the constraints before generating anything. @@ -706,7 +708,7 @@ archgate adr domain add security SEC archgate adr domain remove security ``` -Custom domain → prefix mappings persist in `.archgate/config.json` and are merged with the built-ins at read time. A registered custom domain behaves exactly like a built-in: `archgate adr create --domain security` auto-generates IDs like `SEC-001`, and `archgate adr list --domain security` filters to those ADRs. +Custom domain-to-prefix mappings persist in [`.archgate/config.json`](/reference/configuration/) and are merged with the built-ins at read time. A registered custom domain behaves exactly like a built-in: `archgate adr create --domain security` auto-generates IDs like `SEC-001`, and `archgate adr list --domain security` filters to those ADRs. ### Naming rules @@ -2186,7 +2188,7 @@ You will be prompted for: 2. **Title** -- a short, descriptive name for the decision 3. **File patterns** -- optional comma-separated globs that scope rule checking (e.g., `src/commands/**/*.ts`) -The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/`. +The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/` (or a [custom directory](/reference/configuration/#custom-adr-directory) if configured). ### Non-interactive mode @@ -2486,6 +2488,9 @@ Rules are TypeScript functions that check your codebase for ADR compliance. They ARCH-001-command-structure.rules.ts # The automated checks ``` +:::note +Rules files always live next to their companion ADR files. If you configure a [custom ADR directory](/reference/configuration/#custom-adr-directory), place rules files there too. + ## Basic setup Every rules file exports a default plain object typed with `satisfies RuleSet`. Each key in the `rules` object becomes a rule ID, and each rule has a `description` and an async `check` function that receives a context object. @@ -2778,7 +2783,10 @@ ARCH-006/no-unapproved-deps Source: https://cli.archgate.dev/reference/adr-schema/ -Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. +Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` (by default) with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. + +:::note +The ADR directory can be customized via `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. ## Frontmatter Schema @@ -3597,6 +3605,9 @@ When `--editor cursor` is used, the output shows `.cursor/` instead of `.claude/ archgate.config.ts # Archgate configuration ``` +:::tip +After initialization, you can configure a custom directory for ADRs by editing `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. + --- ## Reference: archgate login @@ -4029,6 +4040,100 @@ Archgate upgraded to 0.4.0 successfully. --- +## Reference: Configuration + +Source: https://cli.archgate.dev/reference/configuration/ + +The `.archgate/config.json` file stores project-level configuration that is committed to version control and shared across the team. + +This file is created automatically by `archgate init` (when custom domains are registered) or when you manually add configuration. It lives inside the `.archgate/` directory at your project root. + +## Schema + +```json +{ + "domains": { "security": "SEC", "compliance": "COMP" }, + "paths": { "adrs": "docs/adrs", "rules": "docs/adrs" } +} +``` + +### `domains` + +Custom domain-to-prefix mappings. See [Custom Domains](/concepts/domains/#custom-domains) for details. + +| Key | Type | Description | +| ------ | -------- | ------------------------------------------------------------------------------------------- | +| _name_ | `string` | Domain name (lowercase kebab-case, 2-32 chars) maps to an ID prefix (uppercase, 2-10 chars) | + +These are merged with the built-in domains (`backend`, `frontend`, `data`, `architecture`, `general`) at read time. Custom entries cannot override built-in names or prefixes. + +### `paths` + +Override default directories for ADRs and rules. + +| Field | Type | Default | Description | +| ------- | -------- | ---------------- | ----------------------------------------- | +| `adrs` | `string` | `.archgate/adrs` | Relative path to the ADR directory | +| `rules` | `string` | `.archgate/lint` | Relative path to the rules/lint directory | + +Both fields are optional. When omitted, the default `.archgate/adrs/` and `.archgate/lint/` directories are used. + +#### Path validation + +- Paths **must be relative** to the project root -- absolute paths (e.g., `/docs/adrs`, `C:\docs\adrs`) are rejected. +- Paths **must not contain `..` segments** -- traversal above the project root is not allowed (e.g., `../other-repo/adrs` is rejected). +- Paths use forward slashes (`/`) as separators, matching standard glob conventions. + +## Custom ADR directory + +By default, ADRs live in `.archgate/adrs/`. To store them in a different directory (e.g., `docs/adrs/`), add a `paths` section to `.archgate/config.json`: + +```json +{ "paths": { "adrs": "docs/adrs" } } +``` + +After adding the configuration: + +1. Create the target directory (e.g., `mkdir -p docs/adrs`) +2. Move existing ADR files and their companion `.rules.ts` files from `.archgate/adrs/` to the new directory +3. Run `archgate check` to verify the rules still load correctly + +All CLI commands (`archgate adr list`, `archgate adr create`, `archgate check`, `archgate review-context`) automatically read the configured directory. + +:::caution +The `.archgate/` directory must still exist -- it is the project marker used by the CLI to locate your project root. Do not delete it after configuring custom paths. + +### Example: monorepo documentation folder + +A common pattern is placing ADRs alongside other documentation: + +``` +my-project/ + .archgate/ + config.json # { "paths": { "adrs": "docs/adrs" } } + lint/ + rules.d.ts + docs/ + adrs/ + ARCH-001-api-design.md + ARCH-001-api-design.rules.ts + BE-001-database-access.md + BE-001-database-access.rules.ts + rules.d.ts # auto-generated by archgate check + guides/ + ... + src/ + ... +``` + +## Notes + +- The `paths` configuration is a **team-wide setting** -- it is committed to version control and applies to all team members. There is no user-level override for ADR paths. +- Changing the configuration requires manually editing `.archgate/config.json` after running `archgate init`. +- The `rules.d.ts` type definitions file is automatically written to both `.archgate/` and the parent of the configured ADR directory, so companion `.rules.ts` files resolve their `/// ` directive correctly. + +--- + ## Reference: Privacy Policy Source: https://cli.archgate.dev/reference/privacy-policy/ From ce2a85f04f8ed350bae0d421d14c060c1208aaad Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 11 May 2026 01:00:44 +0200 Subject: [PATCH 3/5] docs: remove promotional cross-links for custom ADR directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom directory feature covers a special case — no need to advertise it across every page. Keep it documented only in the Configuration reference page where users who need it will find it. Signed-off-by: Rhuan Barreto --- docs/src/content/docs/concepts/adrs.mdx | 6 +----- docs/src/content/docs/getting-started/quick-start.mdx | 2 +- docs/src/content/docs/guides/writing-adrs.mdx | 2 +- docs/src/content/docs/guides/writing-rules.mdx | 4 ---- docs/src/content/docs/pt-br/concepts/adrs.mdx | 6 +----- docs/src/content/docs/pt-br/getting-started/quick-start.mdx | 2 +- docs/src/content/docs/pt-br/guides/writing-adrs.mdx | 2 +- docs/src/content/docs/pt-br/guides/writing-rules.mdx | 4 ---- docs/src/content/docs/pt-br/reference/adr-schema.mdx | 6 +----- docs/src/content/docs/pt-br/reference/cli/init.mdx | 4 ---- docs/src/content/docs/reference/adr-schema.mdx | 6 +----- docs/src/content/docs/reference/cli/init.mdx | 4 ---- 12 files changed, 8 insertions(+), 40 deletions(-) diff --git a/docs/src/content/docs/concepts/adrs.mdx b/docs/src/content/docs/concepts/adrs.mdx index c4b18e3a..73be5c60 100644 --- a/docs/src/content/docs/concepts/adrs.mdx +++ b/docs/src/content/docs/concepts/adrs.mdx @@ -11,11 +11,7 @@ Archgate builds on the ADR concept by giving each decision two expressions: a ** ### ADR as Document -The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/` by default. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. - -:::tip[Custom ADR directory] -ADRs can be stored in a different directory (e.g., `docs/adrs/`) by configuring the `paths.adrs` field in `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. -::: +The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/`. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. Both humans and AI agents consume this document. When an AI coding agent is about to write code, it reads the relevant ADRs to understand the constraints before generating anything. diff --git a/docs/src/content/docs/getting-started/quick-start.mdx b/docs/src/content/docs/getting-started/quick-start.mdx index 8a588342..7da41c0b 100644 --- a/docs/src/content/docs/getting-started/quick-start.mdx +++ b/docs/src/content/docs/getting-started/quick-start.mdx @@ -37,7 +37,7 @@ This creates the `.archgate/` directory with the following structure: archgate.config.ts # Archgate configuration ``` -The generated files give you a working example to build on. The default ADR directory is `.archgate/adrs/`, but it can be [customized](/reference/configuration/#custom-adr-directory) after initialization. +The generated files give you a working example to build on. ## 3. Edit the example ADR diff --git a/docs/src/content/docs/guides/writing-adrs.mdx b/docs/src/content/docs/guides/writing-adrs.mdx index 8cb340c4..3c713d63 100644 --- a/docs/src/content/docs/guides/writing-adrs.mdx +++ b/docs/src/content/docs/guides/writing-adrs.mdx @@ -21,7 +21,7 @@ You will be prompted for: 2. **Title** -- a short, descriptive name for the decision 3. **File patterns** -- optional comma-separated globs that scope rule checking (e.g., `src/commands/**/*.ts`) -The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/` (or a [custom directory](/reference/configuration/#custom-adr-directory) if configured). +The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/`. ### Non-interactive mode diff --git a/docs/src/content/docs/guides/writing-rules.mdx b/docs/src/content/docs/guides/writing-rules.mdx index 1c5cd285..25d5875f 100644 --- a/docs/src/content/docs/guides/writing-rules.mdx +++ b/docs/src/content/docs/guides/writing-rules.mdx @@ -11,10 +11,6 @@ Rules are TypeScript functions that check your codebase for ADR compliance. They ARCH-001-command-structure.rules.ts # The automated checks ``` -:::note -Rules files always live next to their companion ADR files. If you configure a [custom ADR directory](/reference/configuration/#custom-adr-directory), place rules files there too. -::: - ## Basic setup Every rules file exports a default plain object typed with `satisfies RuleSet`. Each key in the `rules` object becomes a rule ID, and each rule has a `description` and an async `check` function that receives a context object. diff --git a/docs/src/content/docs/pt-br/concepts/adrs.mdx b/docs/src/content/docs/pt-br/concepts/adrs.mdx index 5beb2146..6044d5b5 100644 --- a/docs/src/content/docs/pt-br/concepts/adrs.mdx +++ b/docs/src/content/docs/pt-br/concepts/adrs.mdx @@ -11,11 +11,7 @@ O Archgate expande o conceito de ADR dando a cada decisão duas expressões: um ### ADR como Documento -O documento é um arquivo Markdown com frontmatter YAML armazenado em `.archgate/adrs/` por padrão. Ele descreve a decisão em linguagem simples: qual problema resolve, quais alternativas foram consideradas, o que a equipe decidiu, e quais consequências seguem. - -:::tip[Diretório personalizado de ADRs] -ADRs podem ser armazenados em um diretório diferente (ex: `docs/adrs/`) configurando o campo `paths.adrs` em `.archgate/config.json`. Veja a [referência de Configuração](/reference/configuration/#diretorio-personalizado-de-adrs) para detalhes. -::: +O documento é um arquivo Markdown com frontmatter YAML armazenado em `.archgate/adrs/`. Ele descreve a decisão em linguagem simples: qual problema resolve, quais alternativas foram consideradas, o que a equipe decidiu, e quais consequências seguem. Tanto humanos quanto agentes de IA consomem esse documento. Quando um agente de IA de codificação está prestes a escrever código, ele lê os ADRs relevantes para entender as restrições antes de gerar qualquer coisa. diff --git a/docs/src/content/docs/pt-br/getting-started/quick-start.mdx b/docs/src/content/docs/pt-br/getting-started/quick-start.mdx index 5b185fa3..76a47720 100644 --- a/docs/src/content/docs/pt-br/getting-started/quick-start.mdx +++ b/docs/src/content/docs/pt-br/getting-started/quick-start.mdx @@ -37,7 +37,7 @@ Isso cria o diretório `.archgate/` com a seguinte estrutura: archgate.config.ts # Archgate configuration ``` -Os arquivos gerados fornecem um exemplo funcional para você construir a partir dele. O diretório padrão de ADRs é `.archgate/adrs/`, mas pode ser [personalizado](/reference/configuration/#diretorio-personalizado-de-adrs) após a inicialização. +Os arquivos gerados fornecem um exemplo funcional para você construir a partir dele. ## 3. Editar o ADR de exemplo diff --git a/docs/src/content/docs/pt-br/guides/writing-adrs.mdx b/docs/src/content/docs/pt-br/guides/writing-adrs.mdx index 5a297576..622c83d9 100644 --- a/docs/src/content/docs/pt-br/guides/writing-adrs.mdx +++ b/docs/src/content/docs/pt-br/guides/writing-adrs.mdx @@ -21,7 +21,7 @@ Você será solicitado a fornecer: 2. **Title** -- um nome curto e descritivo para a decisão 3. **File patterns** -- globs opcionais separados por vírgula que delimitam o escopo da verificação de regras (ex.: `src/commands/**/*.ts`) -O CLI atribui um ID sequencial baseado no prefixo do domínio (`ARCH-001`, `FE-002`, `BE-003`, etc.) e grava o arquivo em `.archgate/adrs/` (ou em um [diretório personalizado](/reference/configuration/#diretorio-personalizado-de-adrs) se configurado). +O CLI atribui um ID sequencial baseado no prefixo do domínio (`ARCH-001`, `FE-002`, `BE-003`, etc.) e grava o arquivo em `.archgate/adrs/`. ### Modo não-interativo diff --git a/docs/src/content/docs/pt-br/guides/writing-rules.mdx b/docs/src/content/docs/pt-br/guides/writing-rules.mdx index 362d3366..657d7e89 100644 --- a/docs/src/content/docs/pt-br/guides/writing-rules.mdx +++ b/docs/src/content/docs/pt-br/guides/writing-rules.mdx @@ -11,10 +11,6 @@ Regras são funções TypeScript que verificam seu codebase quanto à conformida ARCH-001-command-structure.rules.ts # The automated checks ``` -:::note -Arquivos de regras sempre ficam ao lado de seus arquivos ADR complementares. Se voce configurar um [diretório personalizado de ADRs](/reference/configuration/#diretorio-personalizado-de-adrs), coloque os arquivos de regras lá também. -::: - ## Configuração básica Todo arquivo de regras exporta por padrão um objeto simples tipado com `satisfies RuleSet`. Cada chave no objeto `rules` se torna um ID de regra, e cada regra tem uma `description` e uma função async `check` que recebe um objeto de contexto. diff --git a/docs/src/content/docs/pt-br/reference/adr-schema.mdx b/docs/src/content/docs/pt-br/reference/adr-schema.mdx index e52e55e9..3003460b 100644 --- a/docs/src/content/docs/pt-br/reference/adr-schema.mdx +++ b/docs/src/content/docs/pt-br/reference/adr-schema.mdx @@ -3,11 +3,7 @@ title: Esquema de ADR description: Referência completa do esquema YAML frontmatter e estrutura markdown para Architecture Decision Records do Archgate. Todos os campos e regras de validação. --- -Todo ADR do Archgate é um arquivo Markdown armazenado em `.archgate/adrs/` (por padrão) com frontmatter YAML que define a identidade e o escopo da decisão. Esta página documenta o esquema do frontmatter, a estrutura das seções markdown e o comportamento de validação. - -:::note -O diretório de ADRs pode ser personalizado via `.archgate/config.json`. Veja a [referência de Configuração](/reference/configuration/#diretorio-personalizado-de-adrs) para detalhes. -::: +Todo ADR do Archgate é um arquivo Markdown armazenado em `.archgate/adrs/` com frontmatter YAML que define a identidade e o escopo da decisão. Esta página documenta o esquema do frontmatter, a estrutura das seções markdown e o comportamento de validação. ## Esquema do Frontmatter diff --git a/docs/src/content/docs/pt-br/reference/cli/init.mdx b/docs/src/content/docs/pt-br/reference/cli/init.mdx index 2fef472b..d20b562d 100644 --- a/docs/src/content/docs/pt-br/reference/cli/init.mdx +++ b/docs/src/content/docs/pt-br/reference/cli/init.mdx @@ -51,7 +51,3 @@ Quando `--editor cursor` é usado, a saída mostra `.cursor/` em vez de `.claude lint/ archgate.config.ts # Archgate configuration ``` - -:::tip -Após a inicialização, você pode configurar um diretório personalizado para ADRs editando `.archgate/config.json`. Veja a [referência de Configuração](/reference/configuration/#diretorio-personalizado-de-adrs) para detalhes. -::: diff --git a/docs/src/content/docs/reference/adr-schema.mdx b/docs/src/content/docs/reference/adr-schema.mdx index 966e373b..2a7d2243 100644 --- a/docs/src/content/docs/reference/adr-schema.mdx +++ b/docs/src/content/docs/reference/adr-schema.mdx @@ -3,11 +3,7 @@ title: ADR Schema description: Complete YAML frontmatter schema and markdown structure reference for Archgate Architecture Decision Records. All fields and validation rules explained. --- -Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` (by default) with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. - -:::note -The ADR directory can be customized via `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. -::: +Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. ## Frontmatter Schema diff --git a/docs/src/content/docs/reference/cli/init.mdx b/docs/src/content/docs/reference/cli/init.mdx index 0f33b8b1..46a4de4c 100644 --- a/docs/src/content/docs/reference/cli/init.mdx +++ b/docs/src/content/docs/reference/cli/init.mdx @@ -51,7 +51,3 @@ When `--editor cursor` is used, the output shows `.cursor/` instead of `.claude/ lint/ archgate.config.ts # Archgate configuration ``` - -:::tip -After initialization, you can configure a custom directory for ADRs by editing `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. -::: From 122d247296286ca38c5023d7cac9e3a54f0a345e Mon Sep 17 00:00:00 2001 From: rhuanbarreto <283004+rhuanbarreto@users.noreply.github.com> Date: Sun, 10 May 2026 23:01:55 +0000 Subject: [PATCH 4/5] docs: regenerate llms-full.txt Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/public/llms-full.txt | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index e36225c5..4333083b 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -307,7 +307,7 @@ This creates the `.archgate/` directory with the following structure: archgate.config.ts # Archgate configuration ``` -The generated files give you a working example to build on. The default ADR directory is `.archgate/adrs/`, but it can be [customized](/reference/configuration/#custom-adr-directory) after initialization. +The generated files give you a working example to build on. ## 3. Edit the example ADR @@ -427,9 +427,7 @@ Archgate builds on the ADR concept by giving each decision two expressions: a ** ### ADR as Document -The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/` by default. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. - -ADRs can be stored in a different directory (e.g., `docs/adrs/`) by configuring the `paths.adrs` field in `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. +The document is a Markdown file with YAML frontmatter stored in `.archgate/adrs/`. It describes the decision in plain language: what problem it solves, what alternatives were considered, what the team decided, and what consequences follow. Both humans and AI agents consume this document. When an AI coding agent is about to write code, it reads the relevant ADRs to understand the constraints before generating anything. @@ -2188,7 +2186,7 @@ You will be prompted for: 2. **Title** -- a short, descriptive name for the decision 3. **File patterns** -- optional comma-separated globs that scope rule checking (e.g., `src/commands/**/*.ts`) -The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/` (or a [custom directory](/reference/configuration/#custom-adr-directory) if configured). +The CLI assigns a sequential ID based on the domain prefix (`ARCH-001`, `FE-002`, `BE-003`, etc.) and writes the file to `.archgate/adrs/`. ### Non-interactive mode @@ -2488,9 +2486,6 @@ Rules are TypeScript functions that check your codebase for ADR compliance. They ARCH-001-command-structure.rules.ts # The automated checks ``` -:::note -Rules files always live next to their companion ADR files. If you configure a [custom ADR directory](/reference/configuration/#custom-adr-directory), place rules files there too. - ## Basic setup Every rules file exports a default plain object typed with `satisfies RuleSet`. Each key in the `rules` object becomes a rule ID, and each rule has a `description` and an async `check` function that receives a context object. @@ -2783,10 +2778,7 @@ ARCH-006/no-unapproved-deps Source: https://cli.archgate.dev/reference/adr-schema/ -Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` (by default) with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. - -:::note -The ADR directory can be customized via `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. +Every Archgate ADR is a Markdown file stored in `.archgate/adrs/` with YAML frontmatter that defines the decision's identity and scope. This page documents the frontmatter schema, markdown section structure, and validation behavior. ## Frontmatter Schema @@ -3605,9 +3597,6 @@ When `--editor cursor` is used, the output shows `.cursor/` instead of `.claude/ archgate.config.ts # Archgate configuration ``` -:::tip -After initialization, you can configure a custom directory for ADRs by editing `.archgate/config.json`. See the [Configuration reference](/reference/configuration/#custom-adr-directory) for details. - --- ## Reference: archgate login From 77670036d9a0c82afaf6a724043e4f9db44ddff2 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 11 May 2026 01:21:48 +0200 Subject: [PATCH 5/5] fix(tests): isolate findProjectRoot tests from host ~/.archgate/ Add ARCHGATE_PROJECT_CEILING env var (analogous to git's GIT_CEILING_DIRECTORIES) to prevent findProjectRoot() from walking above the test temp directory. Without this, tests that assert "no project found" fail on machines where ~/.archgate/adrs/ exists. Applied to the integration test harness (subprocess env), command tests (Bun.env), and path unit tests. Signed-off-by: Rhuan Barreto --- src/helpers/paths.ts | 14 +++++++++++++- tests/commands/adr/create.test.ts | 3 +++ tests/commands/adr/list.test.ts | 3 +++ tests/helpers/paths.test.ts | 22 ++++++++++++++++++++-- tests/integration/cli-harness.ts | 4 ++++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/helpers/paths.ts b/src/helpers/paths.ts index 93840829..9204c662 100644 --- a/src/helpers/paths.ts +++ b/src/helpers/paths.ts @@ -2,7 +2,7 @@ // Copyright 2026 Archgate import { existsSync, mkdirSync } from "node:fs"; import { homedir } from "node:os"; -import { join, dirname } from "node:path"; +import { join, dirname, resolve } from "node:path"; import { logDebug } from "./log"; @@ -123,8 +123,15 @@ export function createPathIfNotExists(path: string) { * and would produce false positives. We also avoid matching on * `.archgate/config.json` because `~/.archgate/config.json` stores * telemetry settings. + * + * **Test isolation:** Set `ARCHGATE_PROJECT_CEILING` to a directory path + * to prevent the walk-up from escaping above it — analogous to git's + * `GIT_CEILING_DIRECTORIES`. The ceiling directory itself is still + * checked, but the walk stops there. */ export function findProjectRoot(startDir?: string): string | null { + const ceilingEnv = Bun.env.ARCHGATE_PROJECT_CEILING; + const ceiling = ceilingEnv ? resolve(ceilingEnv) : null; let dir = startDir ?? process.cwd(); while (true) { @@ -134,6 +141,11 @@ export function findProjectRoot(startDir?: string): string | null { return dir; } + // Don't walk above the ceiling directory + if (ceiling && resolve(dir) === ceiling) { + return null; + } + const parent = dirname(dir); if (parent === dir) { return null; diff --git a/tests/commands/adr/create.test.ts b/tests/commands/adr/create.test.ts index 3d138a81..de37ef69 100644 --- a/tests/commands/adr/create.test.ts +++ b/tests/commands/adr/create.test.ts @@ -50,6 +50,8 @@ describe("adr create action handler", () => { beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "archgate-create-test-")); originalCwd = process.cwd(); + // Prevent findProjectRoot() from walking above the temp dir + Bun.env.ARCHGATE_PROJECT_CEILING = tempDir; logSpy = spyOn(console, "log").mockImplementation(() => {}); exitSpy = spyOn(process, "exit").mockImplementation(() => { throw new Error("process.exit"); @@ -58,6 +60,7 @@ describe("adr create action handler", () => { afterEach(() => { process.chdir(originalCwd); + delete Bun.env.ARCHGATE_PROJECT_CEILING; rmSync(tempDir, { recursive: true, force: true }); logSpy.mockRestore(); exitSpy.mockRestore(); diff --git a/tests/commands/adr/list.test.ts b/tests/commands/adr/list.test.ts index 5eb152e1..431aaee5 100644 --- a/tests/commands/adr/list.test.ts +++ b/tests/commands/adr/list.test.ts @@ -72,6 +72,8 @@ describe("adr list action handler", () => { beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "archgate-list-test-")); originalCwd = process.cwd(); + // Prevent findProjectRoot() from walking above the temp dir + Bun.env.ARCHGATE_PROJECT_CEILING = tempDir; logSpy = spyOn(console, "log").mockImplementation(() => {}); exitSpy = spyOn(process, "exit").mockImplementation(() => { throw new Error("process.exit"); @@ -80,6 +82,7 @@ describe("adr list action handler", () => { afterEach(() => { process.chdir(originalCwd); + delete Bun.env.ARCHGATE_PROJECT_CEILING; rmSync(tempDir, { recursive: true, force: true }); logSpy.mockRestore(); exitSpy.mockRestore(); diff --git a/tests/helpers/paths.test.ts b/tests/helpers/paths.test.ts index 4cbbf5ae..18407a15 100644 --- a/tests/helpers/paths.test.ts +++ b/tests/helpers/paths.test.ts @@ -12,9 +12,12 @@ describe("findProjectRoot", () => { beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "archgate-paths-test-")); + // Prevent findProjectRoot() from walking above the temp dir + Bun.env.ARCHGATE_PROJECT_CEILING = tempDir; }); afterEach(() => { + delete Bun.env.ARCHGATE_PROJECT_CEILING; rmSync(tempDir, { recursive: true, force: true }); }); @@ -43,6 +46,12 @@ describe("findProjectRoot", () => { expect(result).toBe(tempDir); }); + test("returns null when no project markers found", () => { + // With the ceiling set, the walk-up is isolated to tempDir + const result = findProjectRoot(tempDir); + expect(result).toBeNull(); + }); + test("does not match directory without .archgate/adrs/ or .archgate/lint/", () => { // A directory with only a bare .archgate/ (like ~/.archgate/ user cache) // should NOT be detected as a project root. @@ -53,7 +62,16 @@ describe("findProjectRoot", () => { mkdirSync(join(parent, ".archgate"), { recursive: true }); const result = findProjectRoot(child); - // Should walk past parent (bare .archgate/) without matching it - expect(result).not.toBe(parent); + expect(result).toBeNull(); + }); + + test("respects ARCHGATE_PROJECT_CEILING to isolate tests", () => { + // The ceiling prevents walk-up past the temp dir, even if + // ~/.archgate/adrs/ exists on the host machine. + const nested = join(tempDir, "deep", "nested"); + mkdirSync(nested, { recursive: true }); + + const result = findProjectRoot(nested); + expect(result).toBeNull(); }); }); diff --git a/tests/integration/cli-harness.ts b/tests/integration/cli-harness.ts index dbfcd6e4..2035b9e8 100644 --- a/tests/integration/cli-harness.ts +++ b/tests/integration/cli-harness.ts @@ -36,6 +36,10 @@ export async function runCli( NO_COLOR: "1", CI: "1", ARCHGATE_TELEMETRY: "0", + // Prevent findProjectRoot() from walking above the test directory + // and discovering the user's ~/.archgate/ — analogous to git's + // GIT_CEILING_DIRECTORIES. + ARCHGATE_PROJECT_CEILING: cwd, ...env, }, });