Skip to main content

Architecture control in CodeGraph

Define an architecture model, evaluate rules on the CPG, and adopt release gates safely. See the examples and checks before applying it.

User Guides

Architecture control checks agreements about dependencies: for example, UI code must not access storage directly, and domain code must not import HTTP handlers. Behavioural tests can still pass after either boundary is crossed. Architecture rules check the direction of the dependencies themselves.

This guide is for developers defining module boundaries and CI engineers adding release checks. The first exercise requires YAML knowledge and access to GoCPG; it does not require knowledge of internal Go tests.

Example and expected result

Consider a Python project where src/ui/ contains interface code and src/storage/ contains data access. We want to forbid direct imports from UI to Storage. A real application would usually introduce a service layer between them; the first rule only needs the two boundaries being checked.

A component groups files or symbols. The model defines these groups, a rule defines allowed relationships, and the CPG records dependencies found in code. A finding must identify the offending relationship and its source location. A release gate also considers analysis completeness, accepted debt, and approved exceptions.

Preparing a rule and checking source code are separate steps. The first step below validates YAML, checks applicability to Python, and describes a component. architecture validate does not read source code or build a CPG. Even applicable: true does not mean the project has no violations.

Prepare the environment

Use a GoCPG build that provides gocpg architecture --help. If the command is missing, the installed binary does not support this section. To build the current source version, run this from the gocpg directory:

go build -o ./bin/gocpg ./cmd/gocpg

The required Go version is declared in gocpg/go.mod. Put the binary directory on PATH or substitute the full binary path for gocpg. YAML validation does not require a local CodeGraph server or a project database. Reading persisted findings with explain requires a CGO/DuckDB build.

Run subsequent commands from the checkout root. The file gocpg/docs/examples/architecture-control/architecture.yaml is included in the source tree. Its complete contents appear below, with no hidden fields needed to complete the example.

Model and first rule

The rule has a stable identifier, example.ui-no-storage. Components have separate uid values: preserve them when renaming the readable id so that result comparisons do not treat a renamed component as a new one.

The paths selector matches files relative to the analysed project root. The components selector refers to declared components. import is an edge type; imports is an analyser capability name. They belong to different fields and are not interchangeable.

spec_version: "2.0"
kind: architecture
id: example.ui-no-storage
version: "1"
fingerprint_version: 1
languages: [python]
architecture:
  source_roots:
    - path: src
      source_set: production
  classification:
    exhaustive: true
    conflict_policy: error
    unclassified_policy: error
    generated_policy: ignore
    minimum_coverage: 1.0
  components:
    - uid: cmp-example-ui
      id: ui
      name: UI
      kind: leaf
      match:
        paths: ["src/ui/**"]
    - uid: cmp-example-storage
      id: storage
      name: Storage
      kind: leaf
      match:
        paths: ["src/storage/**"]
  contract: forbidden_dependency
  from:
    components: [cmp-example-ui]
  to:
    components: [cmp-example-storage]
  relation:
    edge_types: [import]
    reachability: direct
  languages: [python]
  required_capabilities: [imports, source_sets]
  minimum_coverage: 1.0

source_roots identifies production code. Complete, unambiguous classification requires files in that scope to belong to components. Selector conflicts and unknown coverage must be resolved before enforcement. This small example has no other production directories.

forbidden_dependency forbids relationships from from to to. reachability: direct checks immediate imports. Prohibiting reachability through intermediate components requires a transitive rule; changing this meaning must not silently preserve an existing baseline decision.

Check applicability

gocpg architecture validate --file gocpg/docs/examples/architecture-control/architecture.yaml --language python --json

Check these JSON fields; other response fields are omitted here:

{
  "rule_id": "example.ui-no-storage",
  "applicable": true,
  "gate_evaluated": false,
  "reason_codes": ["applicable"]
}

applicable confirms compatibility with the declared language capabilities. gate_evaluated: false says that no release decision was evaluated. For an unselected language such as --language c, the command can exit with code 0 while returning applicable: false and language_not_selected. CI must therefore inspect JSON rather than relying on exit code alone.

Invalid syntax, an unknown YAML field, a missing file, or an invalid CLI flag produces a nonzero exit code. This command group uses --file and --json; --rule and --format json are not supported.

Inspect the component definition

gocpg architecture describe --file gocpg/docs/examples/architecture-control/architecture.yaml --component ui --json

Expect rule_id: example.ui-no-storage, component.uid: cmp-example-ui, and component.id: ui. An unknown component produces an error. Verify that the intended model was loaded before investigating a graph finding.

describe returns the component definition, its parent, and direct children. It does not report actual file classification because it does not read sources.

From a rule to a project check

The next step requires a built CPG and an architecture evaluator run. The architecture group currently registers validate, describe, and explain; it has no general command to execute an arbitrary rule. The scan command serves the separate pattern-rule pipeline. YAML applicability alone does not demonstrate that a full evaluation of this rule is available through the CLI.

With a configured CodeGraph integration, the result must identify the source revision, run, executed rules, and analysis completeness. To inspect a finding, take run_id from that result and fingerprint from the finding itself. Without a persisted run, explain cannot retrieve an explanation. The sections below describe release gate adoption conditions; enabling enforcement requires a verified analysis execution path in your integration.

Adopt it without false green results

  1. Define components and classification, then inspect unclassified entities and conflicts.
  2. Pin exact language-preset and rule-set versions. latest is not an enforcement version.
  3. Start with discovery; it helps tune the model and has no release authority.
  4. Move to advisory, investigate false positives, and baseline only debt that the team has explicitly accepted.
  5. Record temporary exceptions as waivers with an owner, approver, reason, decision reference, and expiry.
  6. Enable enforce_new only after the CPG is fresh, every active rule ran, and classification, resolution, and capability coverage are proven.
  7. Use enforce_all or strict only after known debt is fixed or governed.

discovery and advisory return NOT_APPLICABLE, not release approval. In an enforcing profile, only PASS means release_allowed=true. Violations produce FAIL_VIOLATIONS. A stale CPG, partial run, unknown coverage denominator, missing capability, or untrusted manifest produces BLOCKED_INCOMPLETE.

Baselines, waivers, and trust

A baseline records a known finding for an exact fingerprint version and rule semantic hash. A semantic change moves the decision to needs_review; a local file cannot suppress a finding by itself.

A waiver applies to an exact finding and scope. It is not transferred automatically to a renamed symbol or new fingerprint. Expired, revoked, and unused waivers remain observable findings.

A trusted governance manifest is bound to project, repository, commit, model and rule hashes, validity window, and monotonic revision. Trust keys and revocation state come from the control plane or CI, never from the repository being evaluated.

Full and incremental runs

incremental_complete is valid only when its final fingerprints, baseline/waiver states, and semantic digest equal a full run. Model, public API, baseline, waiver, dependency-edge, and strongly-connected-component changes expand invalidation. A partial enforcement result gets one synchronous full retry; any incomplete result remains BLOCKED_INCOMPLETE.

The CPG refresh publishes a prepared file delta first, then performs bounded semantic reconciliation against the authoritative source manifest. The graph is clean only after stale generations, dangling edges, indexes, state, and covered derived rows have passed integrity checks. Physical compaction is a separate maintenance action. See incremental scan semantics.

Inspect a result

gocpg architecture describe --file gocpg/docs/examples/architecture-control/architecture.yaml --component ui --json
gocpg architecture explain --db ./cpg.duckdb --run-id RUN --fingerprint SHA256 --json

describe returns a component with its parent and direct children. explain reads a persisted canonical finding, locations, and code flow. Both commands are read-only and cannot establish another source of truth.

One guide with reference appendices

The 17 topics form one working cycle. A reader should not open them in an arbitrary order: start with the end-to-end scenario, then use the linked sections for the model, contract, preset, and governance details. The pages below remain normative appendices for exact fields and formats; they are not standalone product explanations.

Map of the 17 topics

1. Architecture model. Define source roots, components, stable uid values, hierarchy, and classification. The output is unambiguous ownership of files and symbols.

2. Rule DSL. Declare kind: architecture, a contract, selectors, languages, capabilities, and coverage. Do not confuse the CPG edge type import with the analyser capability imports.

3. Contract catalog. Choose one of the 12 contracts for dependencies, layers, cycles, APIs, classification completeness, metrics, or resolution coverage.

4. Capability registry. Check which facts the language frontend actually provides. complete confirms required fact families; degraded cannot bypass a missing mandatory capability.

5. Preset authoring. Add a stable id@version, capability manifest, and source-backed fixtures with expected outcomes. A fixture must exercise a real source file, not only the YAML shape.

6. Preset catalog. Resolve a preset by exact ID and semantic version. latest and cross-language aliases are unsuitable for enforcement.

7. Baseline. Record known findings by fingerprint version, rule semantic hash, and exact scope. A semantic rule change requires review.

8. Waiver and trust. A waiver applies to one finding and lifetime. A trusted governance manifest binds commit, model hash, rule hash, scope, revision, signature, and key-revocation state.

9. Release gate profiles. discovery and advisory help tune a model; enforce_new, enforce_all, and strict can block a release. The result must be PASS, FAIL_VIOLATIONS, or BLOCKED_INCOMPLETE with a reason.

10. Incremental scan semantics. Reuse is valid only when fingerprints, semantic digest, and baseline/waiver states match. Model, public API, edge, and strongly connected component changes expand invalidation.

11. Migration. Legacy findings start as discovery/shadow evidence. Moving to enforcement requires comparison and two release cycles; one green local run is not enough.

12. CLI. validate checks applicability, describe shows the model, and explain reads a persisted canonical finding. None of these commands grants release approval by itself.

13. gRPC. A client requests revision-bound results by project, run_id, and fingerprint. The response preserves finding identity and completeness; an empty or mixed graph cannot become a passing response.

14. JSON Schema. Use schemas for result exchange, not as a substitute for the process explanation. Validate run identity, outcome, completeness, location, and lifecycle fields.

15. SARIF mapping. The export preserves rule ID, fingerprint, severity, location, and evidence. SARIF is useful for Code Scanning; the canonical result remains the release authority.

16. Mermaid and PlantUML. A diagram supports model and dependency review. It is a projection, not a second source of truth, and cannot change findings.

17. Troubleshooting. Start with status and reason codes: stale CPG, unknown denominator, missing capability, unresolved edge, and untrusted manifest require evidence repair or blocked enforcement.

Topic index:

Each topic contains its fields, schemas, source references, and technical checks in this guide. The shared CI scenario runs the documented argv on a fresh CLI and checks JSON and negative cases; an internal Go test alone does not prove that a user can follow the procedure.

The current v1 catalog contains three generic templates and eleven language presets: 1C, C, C++, C#, Go, Java, JavaScript, Kotlin, PHP, Python, and TypeScript. A neighbouring language does not imply support: capabilities and fixture qualification are evaluated independently.

Compatibility and recovery

Canonical results are stored in gocpg_architecture_results and headed by cpg_architecture_result_heads. The architecture_adoption_ledger.v1 migration registry permits legacy output only as explicitly labelled discovery or shadow information; legacy_enforcement_allowed=false. Enforcement adoption requires comparison and two_release_cycles, not one successful local run.

See the GoCPG technical analysis for graph construction and type recovery, and the architecture analysis scenario for the user flow.

End-to-end scenario: source to decision

The repository includes the minimal rule gocpg/docs/examples/architecture-control/architecture.yaml. It models UI and Storage and forbids a direct dependency. The scenario has five verifiable steps.

  1. Validate the rule. Run validate and check applicable=true, gate_evaluated=false, and reason_codes containing applicable. This validates input configuration, not an audit result.
  2. Inspect the model. Run describe --component ui and check the stable cmp-example-ui. If the component is missing, later findings are not interpretable.
  3. Build a CPG. For a standalone Python project use gocpg parse --input ./example-project --output ./cpg.duckdb --lang python. For a pull request use gocpg ci-update --input . --output cpg.duckdb --base-ref origin/main --head-ref HEAD --json. The result must identify the revision and graph completeness.
  4. Evaluate the policy. The CLI does not advertise a separate architecture evaluate command. Attach architecture analysis to ci-update with all four explicit inputs: policy, project key, release control, and artifact path. The selected policy binds source roots, components, rules, and classification; GoCPG does not search for a project-specific manifest. Do not use validate or an internal test as a substitute for this step.
  5. Explain and fix a finding. Take run_id and the finding fingerprint from the canonical run, then run explain. After changing the source, repeat a full or valid incremental run. If completeness, capabilities, or denominators are unproven, the result remains BLOCKED_INCOMPLETE.

CPG-only CI fragment

gocpg ci-update --input . --output cpg.duckdb --base-ref origin/main --head-ref HEAD --lang python --json

With no architecture fields, this command only updates the CPG. To request architecture analysis, pass the complete four-field context:

gocpg ci-update --input . --output cpg.duckdb --base-ref origin/main --head-ref HEAD --lang python --json `
  --architecture-policy config/architecture-policy.yaml `
  --architecture-project-key inventory-service `
  --architecture-release-control config/architecture-release-control.yaml `
  --architecture-artifact artifacts/architecture-control.json
gocpg architecture validate --file gocpg/docs/examples/architecture-control/architecture.yaml --language python --json
gocpg architecture describe --file gocpg/docs/examples/architecture-control/architecture.yaml --component ui --json

CI must inspect JSON and the architecture artifact, not only exit codes. For an unselected language, validate may exit 0 while returning applicable=false; that is a diagnostic, not release approval. All four architecture fields absent means that no policy was requested. Any proper subset fails as an invalid request. A complete tuple runs the selected policy; missing, malformed, unsafe, or out-of-repository input fails closed instead of becoming a successful skip.

CI also runs the production test TestCodeGraphPythonCIPipelineBlocksTheRevisionBoundRESTToMCPWitness. It reads entities and dependencies from the DuckDB CPG, applies the policy, and checks that the finding is revision-bound and represented in the release artifact. This is a production-integration check; it does not replace the user-facing commands above with an internal test.

What counts as a fix

In the example, an import from src/ui to src/storage creates a FAIL_VIOLATIONS finding. Moving the access through an allowed application component removes the finding after a new CPG run. If an import cannot be resolved or classification is incomplete, the result does not become green automatically: it remains BLOCKED_INCOMPLETE, and remediation restores graph facts, capabilities, or governance.

Choosing the next section

If files have no owner, start with the model and DSL. If the rule already reports a finding, read the contract catalog, baseline, and waiver/trust. If full and PR results differ, check incremental semantics and capabilities. For CI or Code Scanning output, use JSON Schema and SARIF, while keeping the canonical result as the release authority.