CI gates and constitution enforcement¶
Maps CONSTITUTION.md
articles to local and GitHub
checks. Hard is the default for automatable articles.
How gates fit the broader confidence model (threat model → tests → nuclear
checkpoints): quality-evidence.md.
Modes¶
| Mode | Meaning |
|---|---|
| Hard | Required; merge / ci-local fails |
| Ratchet | Soft ≤1 merge cycle with tracking issue, then hard |
| Review | Checklist / nuclear only |
Article → gate¶
| Article | Hard gates | Review-only |
|---|---|---|
| I Trust / VPC | HTTP import ban on mask/stream/pipeline; offline mask unit test; AST/Semgrep | Future telemetry intent |
| II Fail closed | Tests + AST/Semgrep (no silent passthrough) | New exit-code naming |
| III PII hygiene | gitleaks (pre-commit + default ci-local + lint-and-test); Security AST logging bans; audit tests |
Novel log shapes |
| IV Memory | Streaming tests; file/function limits; agent resource scripts | Bench regressions |
| V No shortcuts | Required status checks; TODO/waiver issue guard | Nuclear judgment |
| VI Secure defaults | pip-audit; Dependabot; Scorecard (Pinned-Dependencies + Token-Permissions); Trivy/hadolint/actionlint via Hygiene gate (container-hygiene); CodeQL (GitHub default setup); SQL AST + allowlist; Semgrep; SHA-pinned Actions on all workflows including release/docs/PyPI; least-privilege GITHUB_TOKEN (workflow contents: read, write scopes only on jobs that need them — see release-infrastructure; Scorecard SARIF via scorecard.yml) |
CVE triage debates |
| VII Honesty | Public language guard; document registry; generate_docs --check |
Marketing tone |
| VIII Architecture | import-linter; C901; file limits; critical coverage floors; jscpd duplicate-code | Boundary redesigns |
| IX Amendments | Constitution registry row; waiver process | Waiver approval |
| X Docs currency | docs/registry.yaml coupling; exit-code anchor sync; env-example sync |
Prose quality |
Local commands¶
./scripts/ci-local.sh # default unit gates (incl. Semgrep, MkDocs link
# boundary, workflow parity, registry, typos, gitleaks)
./scripts/check-duplicates.sh # jscpd on mask/config/secrets/stream (also in default ci-local)
./scripts/ci-local.sh --security # no-op alias (Semgrep already in default)
./scripts/ci-local.sh --mutation # cosmic-ray on mask+config (never default)
./scripts/ci-local.sh --docs # full MkDocs build (needs sibling docs sync)
Default ./scripts/ci-local.sh is sized for a laptop agent budget
(~several minutes with healthy RAM; see .cursor/rules/resource-safety.mdc).
Do not treat it as unbounded — CodeQL, Scorecard, and mutation stay CI-only.
Duplicate code (jscpd)¶
- Config:
.jscpd.json— fail if duplicated lines in scoped packages ≥ 1% (minLines10,minTokens50). - Scope:
src/privaci/{mask,config,secrets,stream}/ - Runner:
./scripts/check-duplicates.sh(requires Node.js 20+ /npx) - Baseline on land: ~0.21% (one small same-file clone in
stream/fetch.py). Ratchet threshold downward when clones are removed (issue #42 if needed).
Article I (no egress on masking path)¶
Hard gates:
- HTTP import ban —
scripts/check_security_ast.pyfails ifmask/,stream/, orpipeline/importhttpx,requests,urllib/urllib.request,urllib3,aiohttp, orhttp/http.client(issue-linked allowlist only). - Packaging import ban — import-linter owns the import-graph contract;
Security AST re-checks
privaci_commercialundersrc/privaci/as defense-in-depth (same single AST walk as other rules). - Offline unit test —
tests/mask/test_offline_mask_path.pymasks with network sockets / DNS blocked. - Logging hygiene (Article III) — Security AST flags logger calls that
interpolate strings or pass PII-ish positional names in scoped packages.
Prefer structured
extra={...}with redacted fields.
Mutation¶
./scripts/mutation-critical.sh/ci-local --mutationruns cosmic-ray onmask/thenconfig/(never default ci-local).- Weekly GitHub workflow is warn-only until kill-score is calibrated (target ≥70% killed on each slice); document promotion in this page when required.
- Resource budget: keep the workflow under ~45 minutes on GitHub-hosted runners; if cosmic-ray cannot meet that after tuning, switch to mutmut with the same scope (design D8).
Complexity ratchet¶
- Ruff C901 max complexity is 10 today; plan is to ratchet toward 8
while burning down
# noqa: C901/ file-limit waivers on issue #42.
Document registry¶
- Manifest:
docs/registry.yaml - Checker:
python scripts/check_doc_registry.py - Diff bases (D11): staged (pre-commit); merge-base with
main(ci-local);--base-sha= PRpull_request.base.sha(GitHub). Coupling skipped on non-PR pushes tomain(--skip-coupling). Fail closed if the local merge-base cannot be resolved. - Waivers:
DOC_REGISTRY_WAIVER: issue #Non the entry (see registry schema). - Changelog: default
optional;requiredonly for operator-visible surfaces.
Active waivers¶
| Gate | Scope | Issue | Notes |
|---|---|---|---|
| File/function limits | Seeded baseline oversizes | #42 | scripts/file_limit_waivers.txt — path-only waives file size, not functions |
| C901 complexity | assert_safe_identifiers, _dispatch_mask_action |
#42 | # noqa: C901 |
| SQL AST allowlist | stream validated-identifier helpers (path:symbol) |
#42 | Prefer symbols over line numbers |
| Coverage floors | mask 96%, secrets 92% baseline | #42 | First-publish exception; burn to 98%+ |
| Mutation | Weekly warn-only | — | Promote after kill-score calibration |
Coverage floors¶
Published after measurement (algorithm in OpenSpec / design D6): target 100%
line coverage for mask/, config/, secrets/. Steady-state floors MUST stay
≥98%; first-publish baselines may be lower with a burn-down issue (see table).
Global ≥85%.
| Package | Floor | Burn-down issue |
|---|---|---|
src/privaci/mask/ |
96% (baseline measured) | #42 → 98% → 100% |
src/privaci/config/ |
98% | #42 → 100% |
src/privaci/secrets/ |
92% (baseline measured) | #42 → 98% → 100% |
Floors file: ci-gates-floors.toml. Baseline seed may be
below 98% only on first publish with a burn-down issue (D6 exception).
How to add a registry row¶
- Add an entry under
entries:indocs/registry.yaml. - Bind
code:globs todocs:paths. - Set
changelog: requiredonly for operator-visible behaviour. - Run
python scripts/check_doc_registry.pyand./scripts/ci-local.sh.
Branch protection¶
After calibration, require: lint-and-test, integration, CodeQL (default
setup), Semgrep,
and the Hygiene gate job from container-hygiene.yml (aggregates
path-filtered Trivy / hadolint / actionlint; no-op success when those paths
are unchanged so the check name stays stable for branch protection).
Follow-ups¶
- Private packaging-repo constitution + gate mirror (OpenSpec task 9.3).
- Mutation warn-only → required after kill-score ≥70% on mask and config.
- Coverage burn-down 92/96 → 98 → 100 (issue #42).
- C901 max 10 → 8 while retiring
# noqa: C901waivers. - Semgrep vs Security AST: AST owns local SQL/logging/packaging/HTTP/eval for
SCAN_PACKAGES(mask/stream/secrets/config/pipeline; HTTP limited tomask/stream/pipeline). Semgrep runs in defaultci-localand the PR Semgrep job (.semgrep.yml+--config=auto+--error); fail closed if neither the CLI nor Docker is available locally. - MkDocs link boundary:
check_mkdocs_doc_links.pyfails relative links that leavedocs/(same class asmkdocs build --strict). Out-of-tree files use absolute GitHub URLs. - Workflow tool parity:
check_ci_workflow_parity.pybansgitleaks-actionand advancedcodeql.yml(org default setup), and pins Semgrep/gitleaks versions against pre-commit. - Dependabot:
docs-buildskips the private commercial clone whenCOMMERCIAL_REPO_READ_TOKENis empty (Dependabot does not see repo secrets unless mirrored under Settings → Secrets and variables → Dependabot). Skip is based on token presence so maintainer “Re-run” still works. Full site build still runs on human PRs with the secret configured. - Dependabot grouping: pip majors go in one weekly
pip-majorPR (minor/patch already grouped); open-PR limits lowered so lockfile PRs do not pile up and conflict after each merge. See.github/dependabot.yml.