An architecture check identifies dependencies that violate agreed component boundaries and connects each finding to source code. The team can inspect results first, record existing debt, and then block new violations in CI. This guide explains how an analysis result becomes a release decision. It is for repository owners and CI operators who already have a validated model and a CPG for the revision they intend to check. Start with model and rules to define components and contracts. Use interfaces and results to inspect the output.
The gocpg architecture validate command only checks the rule’s schema and
language applicability. The production CI path needs a project policy, a
project key, a release-control document and an output artifact. The system
must also prove that the graph, facts, model and rules belong to the evaluated
commit.
Choose a profile for your rollout stage
Start with discovery while defining components. Use advisory to review findings and tune the policy with the team. After recording baseline debt, enforce_new checks new violations. Choose enforce_all when releases must account for existing debt as well, or strict for the full check matrix, including empty selections.
The profile determines how findings affect the decision. Input completeness determines whether the system can make an enforcing decision.
Release Gate Profiles
The gate evaluates evidence completeness before counting findings. It needs a fresh CPG, a complete full or equivalent incremental run, every active rule’s outcome, complete required capabilities, known dependency denominators, classification and resolution coverage, and trusted governance.
| Profile | What it does with findings | Gate decision |
|---|---|---|
discovery |
Helps build and classify the model. | NOT_APPLICABLE |
advisory |
Reports findings while teams review the policy. | NOT_APPLICABLE |
enforce_new |
Blocks new unwaived findings. | PASS or FAIL_VIOLATIONS on complete evidence. |
enforce_all |
Blocks new and known unwaived findings. | PASS or FAIL_VIOLATIONS on complete evidence. |
strict |
Enforces the full matrix, including empty-selection checks. | PASS or FAIL_VIOLATIONS on complete evidence. |
Any enforcing profile returns BLOCKED_INCOMPLETE when its inputs are stale,
partial, unknown or untrusted. Only PASS sets release_allowed=true.
NOT_APPLICABLE means no enforcing decision was requested. A run with zero
resolved dependency facts needs a known denominator; it cannot claim 100%
resolution coverage from an empty result.
For a working repository, a CI call has this shape. Run from the repository
root in PowerShell, with gocpg on PATH, a Git base ref and all four
paths configured for the same project. Replace the paths and project key with
your own; the policy is a repository policy manifest, not the standalone
kind: architecture rule used by architecture validate.
For the first run, create the output directory and parse a CPG from the same repository root. ci-update uses the CPG’s stored source binding; an architecture run with a missing database returns git_source_scope_unavailable.
New-Item -ItemType Directory -Force .gocpg | Out-Null
gocpg parse --input . --output .gocpg/cpg.duckdb --lang python
Then update the graph and produce the architecture artifact:
gocpg ci-update --input . --output .gocpg/cpg.duckdb `
--base-ref origin/main --head-ref HEAD --lang python `
--architecture-policy .gocpg/architecture/policy.yaml `
--architecture-project-key my-repository `
--architecture-release-control .gocpg/architecture/release-control.yaml `
--architecture-artifact .gocpg/architecture/result.json --json
The four architecture flags must be supplied together. A minimal release
control for reviewing findings can select advisory, but it still must satisfy
the current release-control schema and any required repository evidence.
Inspect the artifact’s run identity, completeness, rule outcomes and gate
status. A successful CLI exit alone does not establish PASS. The
adoption guide
explains the broader rollout sequence.
Teaching run with complete inputs
Use PowerShell 7, Git with an author name and email configured, gocpg on PATH, and a CodeGraph source checkout. Replace the two paths below. Create a new directory for the example. Two Python modules provide a real dependency for coverage checks; the shipped registry resolves python/module-boundaries@1.0.0.
$CodeGraphSource = "<path-to-codegraph-source>"
$DemoRoot = "<new-empty-directory>"
New-Item -ItemType Directory -Path $DemoRoot | Out-Null
Set-Location $DemoRoot
New-Item -ItemType Directory -Force app, .gocpg/architecture/fixtures, gocpg/configs/architecture/presets | Out-Null
Copy-Item -Recurse "$CodeGraphSource/gocpg/configs/architecture/presets/v1" gocpg/configs/architecture/presets/v1
@'
from app.helpers import format_greeting
def greet(name):
return format_greeting(name)
'@ | Set-Content -Encoding utf8 app/service.py
@'
def format_greeting(name):
return "Hello, " + name
'@ | Set-Content -Encoding utf8 app/helpers.py
@'
.gocpg/cpg.duckdb*
.gocpg/architecture/result.json
'@ | Set-Content -Encoding utf8 .gitignore
Create a single-component policy and a release-control document selecting discovery. The fixtures paths identify the rule source pair; qualifying that pair is a separate step when preparing a production policy.
@'
schema_version: architecture_policy.v1
id: teaching-service/python
version: 1.0.0
preset_ref: python/module-boundaries@1.0.0
source_paths:
- app/*.py
- app/**/*.py
vocabularies:
layer:
- service
dispositions:
- id: include-production
action: include
paths:
- app/*.py
- app/**/*.py
owner: Architecture Team
reason: Teaching source population.
components:
- uid: service
id: service
name: Service
kind: leaf
tags:
layer:
- service
classifiers:
- id: service-files
component: service
priority: 100
paths:
- app/**
entity_kinds:
- file
- function
- method
- type
rules:
- id: TEAM-CLASSIFICATION-01
version: 1.0.0
contract: exhaustive_classification
mode: discovery
owner: Architecture Team
severity: high
applicability: python
rationale: Classify every source entity.
fixtures:
valid: fixtures/valid.py
invalid: fixtures/invalid.py
spec:
contract: exhaustive_classification
languages:
- python
minimum_coverage: 1
'@ | Set-Content -Encoding utf8 .gocpg/architecture/policy.yaml
@'
schema_version: architecture_release_control.v1
profile: discovery
scope: enforce_new
previous_modes: {}
governance_manifest_path: ''
advisory_cycles_path: ''
rollback_requested: false
rollback_policy_disabled: false
rollback_from_policy_id: ''
rollback_from_policy_version: ''
rollback_from_policy_digest: ''
'@ | Set-Content -Encoding utf8 .gocpg/architecture/release-control.yaml
"def valid(): return 1" | Set-Content -Encoding utf8 .gocpg/architecture/fixtures/valid.py
"def invalid(): return 0" | Set-Content -Encoding utf8 .gocpg/architecture/fixtures/invalid.py
Commit the inputs, parse the CPG, and run the architecture check. The local origin/main ref represents the same teaching revision.
git init -b main
git add .
git commit -m "Teaching architecture input"
git update-ref refs/remotes/origin/main HEAD
gocpg parse --input . --output .gocpg/cpg.duckdb --lang python
gocpg ci-update --input . --output .gocpg/cpg.duckdb `
--base-ref origin/main --head-ref HEAD --lang python `
--architecture-policy .gocpg/architecture/policy.yaml `
--architecture-project-key teaching-service `
--architecture-release-control .gocpg/architecture/release-control.yaml `
--architecture-artifact .gocpg/architecture/result.json --json
$result = Get-Content .gocpg/architecture/result.json -Raw | ConvertFrom-Json
$result | Select-Object status, complete, release_allowed, reason_codes
$result.binding | Select-Object project_key, repository_revision, cpg_repository_revision
Expect exit code 0, status=NOT_APPLICABLE, complete=true, release_allowed=false, and reason non_enforcing_profile. The binding names teaching-service; repository_revision matches cpg_repository_revision and git rev-parse HEAD. Both active_rule_ids and rule_outcome_ids contain TEAM-CLASSIFICATION-01. The discovery profile prepares analysis results for team review.
Create and explain a finding
Continue in the same teaching repository. Assign app/service.py to the service component and app/helpers.py to storage. Rule TEAM-BOUNDARY-01 forbids direct imports from the first component to the second. The existing import on line 1 of app/service.py will produce one finding.
Replace the policy with the following file and add two samples for the new rule. Keep the discovery profile to collect results and inspect dependencies.
@'
schema_version: architecture_policy.v1
id: teaching-service/python
version: 1.0.0
preset_ref: python/module-boundaries@1.0.0
source_paths:
- app/*.py
- app/**/*.py
vocabularies:
layer:
- service
- storage
dispositions:
- id: include-production
action: include
paths:
- app/*.py
- app/**/*.py
owner: Architecture Team
reason: Teaching source population.
components:
- uid: service
id: service
name: Service
kind: leaf
tags:
layer:
- service
- uid: storage
id: storage
name: Storage
kind: leaf
tags:
layer:
- storage
classifiers:
- id: service-files
component: service
priority: 100
paths:
- app/service.py
entity_kinds:
- file
- function
- method
- type
- id: storage-files
component: storage
priority: 100
paths:
- app/helpers.py
entity_kinds:
- file
- function
- method
- type
rules:
- id: TEAM-CLASSIFICATION-01
version: 1.0.0
contract: exhaustive_classification
mode: discovery
owner: Architecture Team
severity: high
applicability: python
rationale: Classify every source entity.
fixtures:
valid: fixtures/valid.py
invalid: fixtures/invalid.py
spec:
contract: exhaustive_classification
languages:
- python
minimum_coverage: 1
- id: TEAM-BOUNDARY-01
version: 1.0.0
contract: forbidden_dependency
mode: discovery
owner: Architecture Team
severity: high
applicability: python
rationale: Teaching forbidden service-to-storage import.
fixtures:
valid: fixtures/boundary-valid.py
invalid: fixtures/boundary-invalid.py
spec:
contract: forbidden_dependency
from:
components:
- service
to:
components:
- storage
relation:
edge_types:
- import
reachability: direct
'@ | Set-Content -Encoding utf8 .gocpg/architecture/policy.yaml
@'
def greet(name):
return name
'@ | Set-Content -Encoding utf8 .gocpg/architecture/fixtures/boundary-valid.py
@'
from app.helpers import format_greeting
def greet(name):
return format_greeting(name)
'@ | Set-Content -Encoding utf8 .gocpg/architecture/fixtures/boundary-invalid.py
Commit the new revision, rebuild the CPG and run the analysis again. These commands read the run ID and fingerprint from the artifact, then explain the finding:
git add .gocpg/architecture
git commit -m "Teaching forbidden dependency"
git update-ref refs/remotes/origin/main HEAD
gocpg parse --input . --output .gocpg/cpg.duckdb --lang python
gocpg ci-update --input . --output .gocpg/cpg.duckdb `
--base-ref origin/main --head-ref HEAD --lang python `
--architecture-policy .gocpg/architecture/policy.yaml `
--architecture-project-key teaching-service `
--architecture-release-control .gocpg/architecture/release-control.yaml `
--architecture-artifact .gocpg/architecture/result.json --json
$artifact = Get-Content .gocpg/architecture/result.json -Raw | ConvertFrom-Json
$RunId = $artifact.binding.run_id
$Fingerprint = $artifact.current_advisory_cycle.rule_fingerprints.'TEAM-BOUNDARY-01'[0]
gocpg architecture explain --db .gocpg/cpg.duckdb --run-id $RunId --fingerprint $Fingerprint --json
Expect finding_count: 1, complete: true and status NOT_APPLICABLE in the artifact. In the explain response, rule_id is TEAM-BOUNDARY-01, the components are service and storage, and the primary source location is app/service.py, line 1. This connects the boundary rule to the specific import and related file.
Use this $RunId to inspect and export results; $Fingerprint selects the finding to explain.
Fix the source and repeat the check
In the same teaching directory, move the simple formatting into greet.
The function still returns Hello, followed by the name, while the direct
service to storage import disappears. Commit the change and rebuild the graph:
@'
def greet(name):
return "Hello, " + name
'@ | Set-Content -Encoding utf8 app/service.py
git add app/service.py
git commit -m "Remove forbidden storage dependency"
git update-ref refs/remotes/origin/main HEAD
gocpg parse --input . --output .gocpg/cpg.duckdb --lang python
gocpg ci-update --input . --output .gocpg/cpg.duckdb `
--base-ref origin/main --head-ref HEAD --lang python `
--architecture-policy .gocpg/architecture/policy.yaml `
--architecture-project-key teaching-service `
--architecture-release-control .gocpg/architecture/release-control.yaml `
--architecture-artifact .gocpg/architecture/result.json --json
$fixed = Get-Content .gocpg/architecture/result.json -Raw | ConvertFrom-Json
$fixed | Select-Object finding_count, complete, status
The new artifact contains finding_count: 0, complete: true, and
status: BLOCKED_INCOMPLETE. Its reason_codes includes
canonical_coverage_incomplete: removing the only import leaves this minimal
project with no relationships for calculating coverage. The rule ran and the
finding was removed; the release check evaluates graph coverage separately.
The revisions in binding.repository_revision and binding.cpg_repository_revision
must match git rev-parse HEAD.
Act on the result
| Result | Next action |
|---|---|
PASS |
Include the architecture result in the release acceptance package. |
FAIL_VIOLATIONS |
Review blocking findings: fix the dependencies or obtain a waiver decision for the current scope. Then rerun the analysis. |
BLOCKED_INCOMPLETE |
Check the CPG revision, rule-result completeness, and governance trust. Refresh missing inputs and rerun. |
NOT_APPLICABLE |
Review the discovery/advisory results. Select an enforcing profile when the policy is ready. |
Baseline Guide
A baseline identifies debt already reviewed for an exact rule, fingerprint
version, finding fingerprint and rule semantic hash. In enforce_new, an exact
known finding is excluded from the new-violation count; enforce_all and
strict still consider known unwaived debt. A baseline acknowledges the
finding. It does not make the dependency acceptable or alter the graph.
Consider a run where UI imports storage. If its fingerprint is reviewed as
existing debt, the next identical run can classify it as known. If the rule
changes from direct to transitive, its semantic hash changes; the old
decision moves to needs_review. If the import disappears and later returns,
its lifecycle is regressed, not silently known again.
To register known debt, run a full analysis and review each finding. Obtain a signed governance manifest from the service that stores project decisions, then rerun the analysis bound to that manifest. A repository-local baseline export helps compare findings between runs; the signed manifest determines which decisions the release check applies.
Save the signed JSON manifest received from the control plane inside the repository, for example at .gocpg/architecture/governance.json. In .gocpg/architecture/release-control.yaml, set governance_manifest_path: .gocpg/architecture/governance.json; the path is relative to the repository root. Repeat ci-update with this release-control file. GoCPG loads the manifest and checks its signature and scope when it evaluates the release gate.
Waiver and Trust Model
A waiver is an exception for one finding and an expiry. It records rule owner, requester, approver, reason, decision reference, fingerprint, semantic hash, fingerprint version and scope. Requester and approver must be different actors. A changed fingerprint, rule, repository or revision needs a fresh decision; a symbol alias cannot transfer a waiver.
The Ed25519 governance payload binds project, repository, commit, branch,
model and rules hashes, validity window and monotonic revision. Trusted public
keys and revocation state come from the control plane or CI configuration.
Validation checks signature, key ID, revocation, validity and exact scope.
Expired, revoked, unused and stale waivers remain visible as findings. A
signature or scope failure blocks enforcement with BLOCKED_INCOMPLETE.
For example, if a waiver was signed for commit A and the policy is evaluated
at commit B, do not copy its fingerprint into a local YAML file to suppress
the finding. Obtain a decision for the current scope after a complete run.
Incremental Scan Semantics
The planner uses changed paths and affected component UIDs to decide whether
to reuse previous work. Model, public API, dependency edge, strongly connected
component, baseline and waiver changes expand invalidation. Unknown
components or a partial invalidation set are errors. The default full-scan
threshold is an affected-component ratio greater than 0.20; a model change
always selects a full run.
incremental_complete has release authority only when fingerprints,
lifecycle states, baseline and waiver states, coverage, semantic digest and
gate decision match a full run for the same inputs. A partial enforcing run
gets one synchronous full retry. If that retry is partial, failed, stale or
mismatched, the result remains BLOCKED_INCOMPLETE.
For example, moving src/ui/ to a new component changes classification and
can affect edges that do not appear in the edited file list. The model change
therefore triggers a full scan. Merely scanning the changed files would miss
relations whose endpoints were reclassified.
Migration Guide
Legacy checks can be migrate, retain advisory, retire or defer.
Discovery and advisory may show labelled legacy evidence, but legacy output
cannot create canonical PASS, finding lifecycle state, baseline, waiver or
release authority.
- Inventory old handlers and rule packs, assigning each one disposition.
- Run old and canonical paths in shadow on the same revision; classify each difference by stable fingerprint and source witness.
- Qualify golden fixtures and resolve unexplained blocking false negatives.
- Observe two real advisory release cycles with stable semantic identity.
- Move consumers to persisted canonical results and prove that legacy enforcement is disabled.
Rollback can return enforcement to advisory. It cannot promote an old checker into release authority. A local parity test demonstrates only its fixture scope, not production adoption.
Implementation checks
From the gocpg module root, these tests verify implementation contracts.
They are not evidence that an arbitrary repository passed its release gate.
go test ./pkg/architecture -run '^TestSignedGovernanceRequiresExactScopeAndSeparationOfDuties$' -count=1
go test ./pkg/architecture -run '^TestNormativeGovernanceRequiresCompleteScopeRevisionAndRevocationState$' -count=1
go test ./pkg/architecture -run '^TestNormativeReleaseGateFiveProfilesAndStateMatrix$' -count=1
go test ./pkg/architecture -run '^TestStory1240FullIncrementalEquivalence$' -count=1
go test ./pkg/architecture -run '^TestCodeGraphReleaseGateRejectsLegacyAuthorityInEnforceNew$' -count=1