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
- Define components and classification, then inspect unclassified entities and conflicts.
- Pin exact language-preset and rule-set versions.
latestis not an enforcement version. - Start with
discovery; it helps tune the model and has no release authority. - Move to
advisory, investigate false positives, and baseline only debt that the team has explicitly accepted. - Record temporary exceptions as waivers with an owner, approver, reason, decision reference, and expiry.
- Enable
enforce_newonly after the CPG is fresh, every active rule ran, and classification, resolution, and capability coverage are proven. - Use
enforce_allorstrictonly 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:
- Architecture model
- Architecture rule DSL
- Contract catalog
- Capability registry
- Preset authoring
- Preset catalog
- Baseline
- Waivers and trust
- Release gate profiles
- Incremental scans
- Migration
- CLI
- gRPC
- JSON Schema
- SARIF mapping
- Mermaid and PlantUML export
- Troubleshooting
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.
- Validate the rule. Run
validateand checkapplicable=true,gate_evaluated=false, andreason_codescontainingapplicable. This validates input configuration, not an audit result. - Inspect the model. Run
describe --component uiand check the stablecmp-example-ui. If the component is missing, later findings are not interpretable. - Build a CPG. For a standalone Python project use
gocpg parse --input ./example-project --output ./cpg.duckdb --lang python. For a pull request usegocpg ci-update --input . --output cpg.duckdb --base-ref origin/main --head-ref HEAD --json. The result must identify the revision and graph completeness. - Evaluate the policy. The CLI does not advertise a separate
architecture evaluatecommand. Attach architecture analysis toci-updatewith 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 usevalidateor an internal test as a substitute for this step. - Explain and fix a finding. Take
run_idand the finding fingerprint from the canonical run, then runexplain. After changing the source, repeat a full or valid incremental run. If completeness, capabilities, or denominators are unproven, the result remainsBLOCKED_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.