Skip to content

Local CI concordance

A green laptop is not automatically a green GitHub Actions badge. This page defines tiers so agents and humans know what "passed" means, and points at the single runner that keeps command strings aligned with CI.

Artifact Role
scripts/ci_local.sh Source of truth for local gate commands and extras lists
Makefile targets ci-fast / ci-core / sync-ci-type / sync-ci-test / type-check-ci Thin wrappers
.github/workflows/ci.yml Server-side jobs
.github/actions/setup-dazzle Frozen uv sync + extras

When you change a CI command or extras list, update scripts/ci_local.sh in the same change (constants at the top of the script cite the CI jobs).


Tiers

Preflight — make preflight-surface (scripts/ci_local.sh preflight-surface)

Mandatory first step of every ship path (also invoked automatically by Tier 0 and Tier 1). Exists to kill the pattern where feature unit tests pass locally while GitHub stays red on unpaid structural/artifact debt.

What it runs CI failure class it blocks
scripts/preflight_surface.py → curated gate modules See table below
Module Debt class Typical fix
test_api_surface_drift MCP/IR/public API baselines dazzle inspect api <lens> --write
test_docs_drift AGENTS MCP table, cli.md groups, generated refs edit docs + dazzle docs generate
test_deferred_imports_ratchet_1438 function-level dazzle.* import growth hoist import or raise baseline with justification
test_import_contracts layer edges (core ↛ page/api_kb) relocate code
test_no_bare_except_pass silent except Exception log with exc_info=True or narrow type
test_ux_catalogue stale catalogue CSS / render crash in previews python scripts/gen_ux_catalogue.py
test_complexity_ratchet cyclomatic/MI regressions refactor or dazzle fitness code --write-baseline
test_clone_ratchet (tree regression nodeid) new/grown clone clusters (URL helpers, etc.) reuse existing fn or dazzle fitness clones --write-baseline
test_hm_package_suite_gate HM gallery stale-dist cd packages/hatchi-maxchi && python site/build_site.py

Non-zero exit → do not ship. The script prints a remediation playbook.

Standalone use mid-change:

make preflight-surface

Tier 0.5 — make ship-surface (scripts/ci_local.sh ship-surface)

Recurrent badge-red classes that used to slip past Tier 0 and only fail after a full ci.yml matrix. Script: scripts/ship_surface.py.

Step CI failure class
bandit -r src/ --severity-level medium lint job B3xx (e.g. B324 hashlib)
pattern_count meta tests patterns.toml [meta].pattern_count
test_no_new_ir_field_orphans IR reader baseline
golden IR snapshot test_simple_dsl_to_ir_snapshot
viewport DRAWER freshness shell toggle selector rot (browser-free)
gen_surface_check catalogue md/css + CONTRACT_SURFACE.md vs generators
test_preflight_recipe_does_not_rebuild_the_venv uv run recreates a non-3.14 .venv mid-suite
test_the_monitor_reports_while_the_gate_still_gates main-hygiene run: folded so \ ate the audit flags

Also runs automatically as the second step of Tier 0 (after preflight). Standalone for mid-edit: make ship-surface.

After a cimonitor repair, new recurrent classes must be promoted into this pack or preflight-surface — fix-only is incomplete.

Gen-surface — make gen-surface-check (scripts/gen_surface_check.py)

Fast check that committed UX catalogue + HM CONTRACT_SURFACE.md match what the generators would emit (no write). Also runs inside ship-surface.

Stale artifact Regen
docs/reference/ux-catalogue.md, docs/assets/dazzle-catalogue.css python scripts/gen_ux_catalogue.py
packages/hatchi-maxchi/CONTRACT_SURFACE.md python packages/hatchi-maxchi/tools/contract_surface.py --write

Rule: regenerate, commit the outputs, then re-run. Do not push with dirty gen files after HM / render / component work.

Path-aware — make ci-changed (scripts/ci_local.sh changed)

scripts/ci_changed.py selects packs from the git diff (origin/main...HEAD plus worktree dirt):

Path touch Pack
examples/**/dsl/** example SPEC bar
src/dazzle/mcp/semantics_kb/** pattern_count
src/dazzle/core/**, IR snapshots golden IR + IR orphans
shell / viewport viewport unit + topbar
packages/hatchi-maxchi/** hm-surface (contract + catalogue + package suite; note sibling visual)
src/dazzle/render/**, kanban/components render-catalogue
src/dazzle/http/** http-ratchets (deferred import + clone + action URL)
src/** bandit medium on src/

Use mid-edit for a fast loop; does not replace Tier 0 for ship.

Push gate — make push-gate (scripts/push_gate.py)

Before every git push to main. Machine-enforced process control from the 2026-07-28 CI autopsy (cancel storm + skipped gates + HM visual plane):

Check Blocks when
Stamp No/stale stamp, or worktree fingerprint ≠ stamp (gates not re-run after edit)
Min tier Stamp tier below required (make push-gate defaults to tier 0 = ci-fast)
Throttle ≥4 commits on origin/main in the last hour, or <8m since newest main commit
CI tip Latest main ci.yml is still in_progress / queued
HM plane Diff touches gallery CSS/JS/baselines and gen-surface is dirty; optional --require-hm-green

Stamp is written automatically at the end of ci_local.sh tier0 / tier1. Manual:

python scripts/push_gate.py record --tier surface   # after preflight+ship-surface only
python scripts/push_gate.py record --tier 0         # after ci-fast
python scripts/push_gate.py check --min-tier 0
python scripts/push_gate.py check --repair          # cimonitor only
python scripts/push_gate.py check --require-hm-green
python scripts/push_gate.py status

/ship and any loop that pushes must run this and refuse push on non-zero.

Tier 0 — make ci-fast (scripts/ci_local.sh tier0)

Default for /ship. Budget ~3–4 minutes, no Postgres. Always runs preflight-surface then ship-surface first.

Step Mirrors
preflight-surface Structural debt cluster (see above)
ship-surface Recurrent badge-red pack (bandit + SPEC/IR/viewport)
ruff check --fix + ruff format CI lint (local mutates; CI is check-only)
mypy src/dazzle CI type-check command (extras may still differ)
pytest tests/unit -m gate Full gate suite (superset of preflight modules)
mkdocs build --strict docs.yml build

Does not include full unit matrix, multi-version Python, Postgres, Playwright walks, or service-backed jobs.

Tier 1 — make ci-core (scripts/ci_local.sh tier1)

Required before release tags (/ship minor, /ship major, or any bump of pyproject.toml version). Closer to GitHub core jobs. Always runs preflight-surface first (after sync, before the long unit matrix).

Step Mirrors
uv sync --frozen with CI python-tests then type-check extras, Python 3.12 setup-dazzle
preflight-surface Structural debt cluster
python scripts/build_dist.py CI builds gitignored dist/ before tests
ruff check + ruff format --check lint
mypy src/dazzle after type extras type-check
CSS clip + raw-ramp + dazzle coverage --fail-on-uncovered lint extras
bandit (medium, all of src/) + pip-audit hard-fail lint + security-tests
JWT fuzz + shapes RBAC matrix security-tests unique gates
pytest -n auto --dist loadgroup -m "not e2e" python-tests (single local version)
mkdocs build --strict docs.yml

Environment knobs:

  • CI_LOCAL_SKIP_SYNC=1 — skip frozen sync (uses current .venv; concordance not guaranteed)
  • CI_LOCAL_COVERAGE=1 — enable coverage on the unit step
  • CI_LOCAL_PYTHON=3.12 — override sync Python (default 3.12)

Tier 2 — still GitHub-only (not in ci_local.sh yet)

These routinely fail after a Tier-0-only ship:

  • Python matrix 3.12 / 3.13 / 3.14
  • postgres-tests (service container)
  • e2e-runtime, e2e-smoke (example validate loop is partially make-able)
  • interaction-walks / viewport (Playwright + Postgres)
  • guide-walks (12-app matrix)
  • contracts-gate (ux verify --contracts --managed on support_tickets)
  • homebrew-validation (macOS)

See ci.yml job names for the authoritative list.


Extras cheatsheet (must match CI)

Copied into scripts/ci_local.sh — do not invent a third list.

Job Extras
type-check dev,llm,mcp,postgres,viewport,perf
python-tests dev,llm,mcp,postgres,perf,test-full
lint dev,llm,mcp,postgres,perf
security-tests dev,sso,postgres,perf
setup-dazzle default dev,llm,mcp,postgres,perf

Install type-check env only:

make sync-ci-type
make type-check-ci

Agent skills

Skill Default tier
/ship Tier 0 (make ci-fast = preflight + ship-surface + ruff/mypy/gate/docs); Tier 1 for minor/major or version bumps; optional make ci-changed mid-edit
/cimonitor Repair badge; close the loop by promoting new failure classes into ship-surface/preflight
/check Opportunistic per changed files (make ci-changed); prefer full unit when Python changed; make ci-core when asked for release-grade

Why concordance breaks

  1. Extras — thin local venv → mypy lies (warn_return_any / unused ignores).
  2. Tier 0 is intentional subset — gate suite ≠ full python-tests.
  3. No Postgres / Playwright locally unless you add services.
  4. OS — macOS vs ubuntu-latest.
  5. Three legacy "CI" paths — prefer ci-fast / ci-core over old make ci (no frozen extras). make security is no longer one of them: it delegates to bash scripts/ci_local.sh security, which runs the same bandit + pip-audit implementation as the security-tests job. It used to carry its own uv run pip-audit --strict --desc on || true — a fourth copy of the audit that could not fail, and whose stray on argument made it a different command from the CI one.
  6. The pip-audit verdict has exactly one implementation — scripts/pip_audit.py, shared by the security-tests job, the main-hygiene monitor, and ci_local.sh. make security delegates too. The three hand-rolled copies had already drifted into disagreeing about what "clean" means; one of them filed issue #1745 reporting advisories that did not exist, after an advisory-ID pattern matched 1 row in 124. A gate whose meaning depends on which file you read is not a gate, so tests/unit/test_pip_audit.py (a -m gate test, run by make ci-fast) fails if any caller re-spells the invocation or duplicates the suppression.
  7. Unpaid surface debt — MCP/CLI/IR changes without regenerating baselines and docs. This is what preflight-surface exists to catch before push. Stacking feature commits on a red tip multiplies the same failures across every matrix cell.
  8. uv run against the pinned .python-version. The Makefile exports UV_MANAGED_PYTHON=1 and .python-version is 3.14. A recipe the suite shells out to (make test-ux-preflight) that calls $(UV) run deletes a 3.12 or 3.13 .venv and recreates it as 3.14 with the default extras. The laptop is already 3.14, so make ci-fast stays green. The preflight recipe calls .venv/bin/python -m, and the python-tests pytest steps set UV_NO_SYNC=1.
  9. A plain multiline run: folds backslashes. main-hygiene's audit step was several lines of python scripts/pip_audit.py --report-only \ without a | block. Actions joined the lines with spaces, the \ escaped that space, and pip_audit.py exited 2 on flags with a leading space. Keep that invocation on one line.

Quick recipes

# Mid-change: only structural debt (fast fail)
make preflight-surface

# Everyday push confidence (includes preflight)
make ci-fast

# Before tagging a release
make ci-core

# Only re-type after deps change
make type-check-ci

# Skip sync when debugging a single test (you accept drift)
CI_LOCAL_SKIP_SYNC=1 bash scripts/ci_local.sh tier1