Skip to main content

Architecture control in CodeGraph

Define component boundaries, check dependencies against a CPG and investigate violations. Apply architecture rules to changes before release.

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.

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

Run architecture analysis through ci-update. Supply the policy, project key, release control, and artifact path. The policy defines components, classification, and rules; release control selects the release-check profile.

Follow the teaching run with complete inputs: it provides Python sources, both configuration files, Git preparation, and CPG commands. Then follow the finding and explanation example. The direct import creates a persisted finding, and explain shows its rule, components, and source line. Together, these examples cover configuration through the analysis result.

For an integration, take run_id from the result and fingerprint from the finding. The persisted result also identifies the source revision, executed rules, and analysis completeness. Use validate and describe to check the model before running analysis, then explain to inspect the result.

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 three detailed references

The 17 normative topics form one working cycle. Start with the scenario above, then use model and rules to design the policy, execution and governance to run it, and interfaces and results to inspect the output. Each topic below has one section in those references; the numbered map remains here for orientation and for older links.

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. Select the database with db and a persisted run with run_id. To explain one finding, also supply its fingerprint. The response contains the source revision, finding identity and analysis completeness.

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, lifecycle state, source locations and evidence. Each finding receives level error. Use SARIF in Code Scanning and read the release decision from the canonical result.

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:

The three linked references contain the detailed fields, schemas, source references, and technical checks. This guide keeps the workflow and topic map.

The current v1 catalog contains three generic templates and eleven language presets: 1C, C, C++, C#, Go, Java, JavaScript, Kotlin, PHP, Python, and TypeScript. Each language has independent capability checks and source-backed fixture qualification.

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

Run these commands from the analysed Git repository root. Before the first incremental update, create the CPG for the same directory and language:

gocpg parse --input . --output cpg.duckdb --lang python

GoCPG saves the parse scope and current Git revision in the database. Check that parsing finishes without errors and creates cpg.duckdb, then update that same database with the command below. The repository must contain origin/main and HEAD. Expect JSON with success: true, base_commit and head_commit; when the revisions match, the changed-file count is zero.

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:

The following is a configuration template, not a complete UI/Storage exercise. Replace config/architecture-policy.yaml and config/architecture-release-control.yaml with existing, validated files inside the checked repository, set your project key, and create the artifact directory. The small architecture.yaml above is a rule, not a complete policy or release-control file. Use the linked execution reference to prepare these inputs before running the template.

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.

What counts as a fix

Verify a fix by running analysis for the changed revision. Compare findings, executed rules, completeness, and the final status.

In the tested continuation, formatting moves into greet and the direct import disappears. Rebuilding the graph changes finding_count from 1 to 0 while preserving the function result. This minimal project loses its only relationship, so the coverage check returns BLOCKED_INCOMPLETE with reason canonical_coverage_incomplete. The result reports finding removal and coverage separately.

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.