Documentation › Concepts
Gates: DCO, CLA, complexity
Beyond the five evaluation dimensions
(Evaluations + dimensions), a PR
faces a small set of binary gates — each one says
either "this PR is OK to merge" or "no, fix X." Gates are
configured in governance.pxf and surface as GitHub check runs
on the PR.
This doc covers what each gate checks, how enforcement modes control what a failure does, and how to wire each gate on.
Enforcement modes: observe, warn, block
Structural gates (complexity, coverage, perf, Static Analysis
Results Interchange Format (SARIF), and the supply-chain family)
share one enforcement system. A repo-wide enforcement sets the
default; each domain can override it:
enforcement = "warn" # repo-wide default
domains: [
{ name: "security", path_globs: ["internal/auth/**"], enforcement: "block" }
{ name: "rest", path_globs: ["**/*"] } # inherits "warn"
]
| Mode | What a violation does |
|---|---|
observe |
Recorded in the audit trail + fleet dashboard only. No PR comment, never blocks. |
warn |
Advisory PR comment ("in block mode this PR would be rejected"). Never blocks. |
block |
Rejects the PR — the default when nothing is set. |
Three behaviors worth knowing:
- When one gate result spans several domains, the strictest
resolved mode wins (
block>warn>observe). - In
warn/observe, evaluation keeps going past a violation instead of stopping at the first failed gate — one run surfaces everything that would block. - Every violation lands in the
fleet dashboard's gate-outcomes panel
either way, which is how the rollout play works: turn a gate
on in
warn, watch the would-have-blocked counts for a couple of weeks, and flip toblockwhen the panel's evidence (and its "flip to block?" hint) says the bar is already being met.
The CLA/DCO gate's own switch
The compliance gate predates this system and keeps its own pair
on the cla block: block (failing check run; branch
protection refuses the merge) or flag (neutral check run, PR
can still merge). Most teams move CLA + DCO straight to block;
the structural gates benefit from a warn interlude.
DCO — Developer Certificate of Origin
Requires every commit in the PR to carry a Signed-off-by:
trailer whose email matches the commit author's. git commit --signoff is the standard way to produce one.
cla: {
dco_required: true
enforcement: "block" # or "flag"
exempt_logins: ["dependabot[bot]", "renovate[bot]"]
}
What Steward does:
- Walks every commit in the PR.
- Skips merge commits (
ParentCount > 1) —git merge mainpulls that contributors do during development rarely carry the trailer, and exempting them avoids a noise floor that would mask real violations. - Skips commits whose author login is in
exempt_logins. - Posts a check run named
Steward — DCOwith the result.
The check passes when zero violations remain. On failure, the comment lists each non-signed commit + its author so the contributor knows exactly which commits to rewrite.
What contributors do
git commit -s -m "feat: add the thing" # for new commits
git rebase --signoff HEAD~N # to retro-sign N existing commits
git push --force-with-lease # update the PR
DCO does not require a separate CLA database — the signoff is the legal hook. Many Open Source Software (OSS) projects (Linux, Docker) use it as their compliance baseline; internal teams use it as a lightweight IP-chain check on contractor and joint-venture commits.
CLA — Contributor License Agreement
A heavier compliance hook. The contributor reads a CLA text, the
contributor signs it electronically (a click-through inside
/app), Steward records the signature, and the gate passes when
the signature matches the current CLA text content hash.
To enable:
- Commit
.steward/cla.mdto the default branch — this is the CLA text shown to contributors and is the indicator file Steward uses to detect "this repo has a CLA." - Set the enforcement block:
cla: {
enforcement: "block"
exempt_logins: ["dependabot[bot]"]
template_path: ".steward/cla.md" # default; override only if necessary
version: "v1" # advisory; the content hash is the actual key
}
On each PR, Steward:
- Loads the current CLA text from the default branch and hashes it.
- Looks up the PR author's
app.cla_signaturesrow for this repo at that content hash. Match → signed. Miss → unsigned. - Posts a check run named
Steward — CLAwith the result. On first failure for an opened PR, also posts a one-time comment with a deep link to the signing surface.
Why content hashing
The contributor's signature is tied to the exact text they
agreed to. If you amend .steward/cla.md, every previously-
signed contributor sees an unsigned status until they re-sign
the new text. This is the legally-honest behaviour — a
contributor who agreed to v1 didn't agree to v2.
If your amendments are non-substantive (typo fixes), use the
amendments voting rule in governance.pxf to track them
separately so you can grandfather signatures by policy if
desired.
Exempt logins
Bots that can't click through a CLA need to be exempted. Typical entries:
exempt_logins: ["dependabot[bot]", "renovate[bot]", "github-actions[bot]"]
These commits skip the signature check entirely and the gate passes for the PR if no human contributor is unsigned.
Complexity — cyclomatic ceiling
McCabe cyclomatic complexity per function, computed at the AST
level. Compared against max_cyclomatic_complexity on the
domain the function's file belongs to.
domains: [
{ name: "core", path_globs: ["internal/**"], max_cyclomatic_complexity: 15 }
{ name: "cli", path_globs: ["cmd/**"], max_cyclomatic_complexity: 25 }
{ name: "rest", path_globs: ["**/*"], max_cyclomatic_complexity: 30 }
]
The gate runs on the PR's changed files only — pre-existing violations elsewhere in the codebase aren't dragged in. So the gate effect is "any function YOU edited that's above the ceiling trips the gate" (blocking, warning, or just recorded, per the domain's enforcement mode); old code stays as-is until someone touches it.
Language coverage
Native AST analysis is Go-only today. For the other 9 supported languages, complexity comes through SARIF (a language-specific linter emits cyclomatic warnings as SARIF results; the SARIF gate ingests them).
Per-function reporting
The gate's PR comment lists every offending function with its complexity score and the limit it violated:
internal/api/router.go:HandleRequest — cyclomatic 23 (limit: 15)
internal/api/router.go:dispatch — cyclomatic 18 (limit: 15)
This is intentionally specific so a contributor doesn't have to guess which function tripped the gate.
SARIF — static-analysis ingest
The general-purpose language-agnostic gate. Your CI uploads
SARIF 2.1.0 artifacts (most modern linters can emit them);
Steward parses them and turns each error-severity result into
a violation.
sarif: { enabled: true, artifact_glob: "sarif/*.sarif" }
Filtering happens in this order:
- Severity — only
errorresults are gating.warning/noteare advisory and surface in the dashboard but don't block. - Go carve-out — results in
.gofiles are skipped. The native AST analyzer is authoritative for Go (ADR-007 amended); SARIF would duplicate it. - Domain mapping — the result's file path must map to a
governance.pxfdomain viapath_globs. Out-of-domain results are skipped. - Per-domain waivers — a result's
RuleIDin the matched domain'signored_sarif_rulesis dropped:
{ name: "ui", path_globs: ["app/**"], ignored_sarif_rules: ["semgrep.react.unescaped-html"] }
The check run is named Steward — SARIF. The PR comment
lists each surviving violation with rule ID, file, line, and the
tool name.
What runs the linter
Steward doesn't bundle a linter. Wire whatever you already use:
- JavaScript/TypeScript: ESLint with
--format @microsoft/eslint-formatter-sarif - Python: Semgrep, Bandit, Pylint
- Java: SpotBugs, PMD, Semgrep
- C#: Roslyn analyzers
- Rust: Clippy (use
cargo clippy --message-format json+ a converter) - C/C++: clang-tidy with the SARIF formatter
Your CI uploads the resulting .sarif files; the
artifact_glob tells Steward where to find them.
Coverage — per-domain floor
{ name: "core", path_globs: ["internal/**"], min_coverage: 0.70 }
Steward compares the PR head's coverage in the domain's files
against min_coverage. A PR that drops coverage below the
floor fails the gate.
Language coverage (here too)
- Go: native. Coverage comes from the standard Go coverage profile your CI produces.
- Other languages: via SARIF. Several coverage tools emit SARIF; the gate reads coverage as a structured result.
Day-one tuning
Set the floor 5–10 points below your current coverage. Otherwise the gate fails every PR on day one and your team trains itself to ignore the verdict. Ratchet up as the codebase improves.
Mission drift + perf (Manifesto)
Two niche gates that live in the manifesto block:
manifesto: {
mission: "Stewardship infra for OSS"
mission_drift_gate: true
blocked_module_globs: ["**/internal/legacy/**"]
max_perf_regression_percent: 10
perf_significance_alpha: 0.05
}
mission_drift_gate— when on, the Large Language Model (LLM) scorer flags PRs that introduce features unrelated to the statedmission. Useful for projects that want to stay focused; off by default.blocked_module_globs— outright rejects any PR that modifies a matching path. The escape hatch for sunsetted code.max_perf_regression_percent— for repos that emit Go benchmark output, rejects PRs whose head benchmark ns/op exceeds base by this percent.perf_significance_alphaadds a Welch's t-test gate (0.05 is the standard cutoff); 0 disables stat-significance.
These are opt-in. Day-one repos can ignore them.
What's next
- Debug a failed evaluation — what to do when a gate fails (or fails unexpectedly).
- Read an evaluation report — the playbook for reading the PR comment that aggregates these gates with the evaluation verdict.
- governance.pxf + domains — the file that configures every gate above.
Steward