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
- 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 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:
- 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
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.
- 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
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.