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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 85 additions & 2 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,12 @@ The contracts family needs a base revision, so it runs in diff mode. When `check
## Design

Purpose:
- Layer boundary enforcement

- Language-neutral architecture boundary enforcement
- Domain and data ownership protection
- Capability and public-surface encapsulation
- Production/test isolation
- Entrypoint reachability and dependency stability warnings
- Separation of concerns heuristics
- Clean-code naming heuristics
- SOLID-oriented heuristics
Expand Down Expand Up @@ -821,7 +826,10 @@ Config keys:
```

Current behavior:
- fails on architecture boundary violations

- fails on configured layer, domain, data-ownership, capability, public-surface, and production/test boundary violations
- can require every module to belong to a configured layer and domain
- warns on configured unreachable modules and stability-direction inversions
- Go targets keep the existing package, import-boundary, declaration-count, type-size, and interface-size heuristics
- Python targets fail on public-to-private imports, direct or transitive entrypoint coupling, and internal import cycles, and warn on overly generic module names
- TypeScript targets warn on overly generic module names, oversized classes, and oversized interfaces or object types using compiler-parsed AST analysis when the semantic runtime is available
Expand Down Expand Up @@ -853,6 +861,81 @@ Language command example:
}
```

### Standalone design policy

Large architecture policies can live outside the main config as a top-level
`DesignRulesConfig` document. Codeguard automatically looks for
`.codeguard/design_rules.yml`, then `.codeguard/design_rules.yaml`, relative to the
project/config root. When the main config is already inside `.codeguard`, the policy is
discovered beside it. A missing auto-discovered file is ignored.

To choose another file, set `checks.design_rules_file`. Relative paths are resolved from
the directory containing the main config:

```yaml
checks:
design: true
design_rules_file: policies/architecture.yml
design_rules:
# Optional local overrides of the shared policy.
stability:
enabled: false
```

An explicit file must exist and must remain within the project/config root, including
after symlinks are resolved. The external file contains the design-rule fields directly;
do not wrap them in `checks.design_rules`:

```yaml
require_boundary_assignment: true
layers:
- name: domain
paths: ["internal/domain/**"]
may_depend_on: [contracts]
- name: contracts
paths: ["internal/contracts/**"]
```

The external policy is the base. Each top-level field explicitly present in inline
`checks.design_rules` replaces the corresponding external field, including explicit
`false`, `0`, and empty list values. Profile defaults fill fields that remain unspecified.
If both auto-discovery names exist, `design_rules.yml` takes precedence; keep only one to
avoid ambiguity. See the complete [standalone policy template](../examples/design_rules.yml).

All architecture policies are opt-in and run only when `checks.design` is enabled. A
non-empty `layers`, `domains`, `capabilities`, or `public_surfaces` list enables that
policy. `production_test`, `reachability`, and `stability` become active when their
section is present unless `enabled: false` is set. `require_boundary_assignment` runs only
when explicitly true. Existing design heuristics and graph rules retain their normal
defaults.

Paths use forward-slash, target-relative matching. A path without glob syntax matches
that path and its descendants; `*`, `?`, character classes, and `**` globs are supported.
External-import policies also match the language's import specifier.

Architecture policy fields:

| Policy | Fields | Behavior |
|---|---|---|
| Layer dependencies | `layers[].name`, `paths`, `may_depend_on`, `deny_depend_on`, `denied_external` | Restricts local dependencies by layer and denies selected external imports. Imports within the same layer are allowed. An empty `may_depend_on` does not create an allowlist. |
| Boundary assignment | `require_boundary_assignment` | Fails modules not covered by every configured boundary dimension: layer, domain, or both. |
| Domains and data ownership | `domains[].name`, `paths`, `public_paths`, `data_paths`, `may_depend_on` | Cross-domain imports must target an allowed domain's public path. Another domain's data paths always remain private. An empty `may_depend_on` denies cross-domain imports. |
| Capabilities | `capabilities[].name`, `imports`, `allowed_paths` | Limits database, network, filesystem, cloud SDK, or other sensitive imports to approved adapters. |
| Public surfaces | `public_surfaces[].name`, `paths`, `entrypoints` | Prevents consumers outside a component from deep-importing its private modules. Imports originating inside that surface are allowed. |
| Production/test isolation | `production_test.enabled`, `production_paths`, `test_paths` | Prevents production modules from importing test helpers, mocks, fixtures, or other test-only modules. |
| Reachability | `reachability.enabled`, `entrypoints`, `ignore_paths` | Warns when a production module cannot be reached from a configured entrypoint. `targets[].entrypoints` are included automatically. |
| Stability direction | `stability.enabled`, `minimum_fan_in`, `max_instability_delta`, `ignore_paths` | Warns when a stable, widely depended-on module imports a substantially less stable module. Defaults are fan-in `3` and instability delta `0.35`. |

To migrate an existing inline policy:

1. Copy the contents of `checks.design_rules` into `.codeguard/design_rules.yml`, without
the `checks` or `design_rules` wrapper.
2. Remove the copied inline fields. Auto-discovery needs no additional main-config key;
set `checks.design_rules_file` only for a different filename or location.
3. Keep small repository- or environment-specific inline fields when they should override
the shared policy, then run a full scan before enabling
`require_boundary_assignment` or reachability.

## Security

Purpose:
Expand Down
18 changes: 18 additions & 0 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ if err := codeguard.WriteReport(os.Stdout, report, "json"); err != nil {
}
```

### Loading standalone design policies

`LoadConfigFile` also loads a standalone architecture policy. It auto-discovers
`.codeguard/design_rules.yml` or `.codeguard/design_rules.yaml`, or follows
`checks.design_rules_file` relative to the main config. The external document is loaded as
the base `DesignRulesConfig`; explicitly present inline `checks.design_rules` fields win.

This behavior belongs to file loading. Setting `CheckConfig.DesignRulesFile` on a config
constructed entirely in memory and passing it directly to `Run` does not read the file.
For an in-memory config, populate `CheckConfig.DesignRules` directly using the exported
`DesignRulesConfig`, `DesignLayerConfig`, `DesignDomainConfig`,
`DesignCapabilityConfig`, `DesignPublicSurfaceConfig`,
`DesignProductionTestConfig`, `DesignReachabilityConfig`, and
`DesignStabilityConfig` types.

See [the standalone policy template](../examples/design_rules.yml) for the complete YAML
schema and migration notes in the [Design checks guide](checks.md#standalone-design-policy).

## Verified fix flow

`VerifyFix` and `GenerateVerifiedFix` fail closed. They do not return a patch unless:
Expand Down
176 changes: 176 additions & 0 deletions examples/design_rules.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Standalone Codeguard design policy template.
#
# Put this file at .codeguard/design_rules.yml for automatic discovery, or point
# checks.design_rules_file at it from the main Codeguard config. This document is
# the contents of DesignRulesConfig itself; it is not wrapped in `checks` or
# `design_rules`.
#
# Paths are relative to each target. Adapt these example paths to your repository
# before enabling strict assignment.

# Start false while adopting a policy. Set true after every production module is
# covered by a configured layer and domain.
require_boundary_assignment: false

layers:
- name: domain
paths:
- internal/domain/**
may_depend_on:
- contracts
denied_external:
- database/sql
- net/http
- github.com/aws/**

- name: contracts
paths:
- internal/contracts/**

- name: application
paths:
- internal/application/**
may_depend_on:
- domain
- contracts

- name: adapters
paths:
- internal/adapters/**
may_depend_on:
- application
- domain
- contracts
deny_depend_on:
- delivery

- name: delivery
paths:
- internal/http/**
- internal/workers/**
may_depend_on:
- application
- contracts

- name: bootstrap
paths:
- cmd/**
- internal/bootstrap/**
may_depend_on:
- application
- adapters
- delivery
- contracts

domains:
- name: orders
paths:
- internal/orders/**
public_paths:
- internal/orders/contracts/**
- internal/orders/events/**
data_paths:
- internal/orders/repository/**
- internal/orders/models/**
may_depend_on:
- identity

- name: identity
paths:
- internal/identity/**
public_paths:
- internal/identity/contracts/**
data_paths:
- internal/identity/repository/**
- internal/identity/models/**
# Empty may_depend_on denies cross-domain imports for this domain.

# Only these adapters may import the listed database or network capabilities.
# Import patterns can match local module paths or external package specifiers.
capabilities:
- name: database
imports:
- database/sql
- github.com/jackc/pgx/**
- sqlalchemy
- sqlalchemy/**
- "@prisma/client"
allowed_paths:
- internal/adapters/database/**
- internal/*/repository/**

- name: outbound-network
imports:
- net/http
- requests
- axios
allowed_paths:
- internal/adapters/http/**

# Consumers outside each component must import through an approved entrypoint.
public_surfaces:
- name: orders
paths:
- internal/orders/**
entrypoints:
- internal/orders/contracts/**
- internal/orders/events/**

- name: identity
paths:
- internal/identity/**
entrypoints:
- internal/identity/contracts/**

production_test:
enabled: true
production_paths:
- cmd/**
- internal/**
- src/**
test_paths:
- tests/**
- test/**
- "**/*_test.go"
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/testdata/**"
- "**/fixtures/**"
- "**/mocks/**"

reachability:
enabled: true
entrypoints:
- cmd/**/main.go
- src/index.ts
- src/main.py
ignore_paths:
- "**/*_test.go"
- tests/**
- "**/generated/**"

stability:
enabled: true
minimum_fan_in: 3
max_instability_delta: 0.35
ignore_paths:
- "**/generated/**"
- "**/*_test.go"

# Existing design heuristics and dependency-graph checks can live in this file
# too. Inline checks.design_rules values in the main config override the matching
# top-level fields here, including false, zero, and empty-list values.
detect_import_cycles: true
detect_god_modules: true
god_module_threshold: 24
detect_high_impact_changes: true
high_impact_change_threshold: 20
max_decls_per_file: 12
max_methods_per_type: 8
max_interface_methods: 5
forbidden_package_names:
- util
- utils
- common
- helpers
- misc
3 changes: 3 additions & 0 deletions internal/codeguard/checks/design/design.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ func Run(ctx context.Context, env support.Context) core.SectionResult {
if graph != nil {
findings = append(findings, importCycleFindings(env, graph)...)
findings = append(findings, godModuleFindings(env, graph)...)
findings = append(findings, architectureBoundaryFindings(env, target, graph)...)
findings = append(findings, encapsulationBoundaryFindings(env, target, graph)...)
findings = append(findings, graphPolicyFindings(env, target, graph)...)
graphs = append(graphs, targetModuleGraph{target: target, graph: graph})
}
findings = append(findings, commandFindings(ctx, env, target)...)
Expand Down
Loading
Loading