diff --git a/.github/workflows/loop.yml b/.github/workflows/loop.yml index 5deae93..6259b6e 100644 --- a/.github/workflows/loop.yml +++ b/.github/workflows/loop.yml @@ -22,6 +22,11 @@ # TRIAGE are READ-ONLY on product code and may only ADD/DELETE tests — a # guard step fails the run if any `bhulan/` file changes. # - EVAL runs the existing unit+integration suite as a hard regression gate. +# - A CI-PARITY step after BUILD mirrors ci.yml's non-test gates (ruff, mypy, +# openapi.json drift, frontend gen:api:check): it auto-fixes the mechanical +# ones and regenerates the generated files so the PR this loop opens isn't +# red on lint/type/schema (which, when merged, turned master red every +# cycle). Genuine mypy/lint errors are left for the build agent to fix. # # Trigger: workflow_dispatch ONLY. Needs the CLAUDE_CODE_OAUTH_TOKEN secret. @@ -69,6 +74,17 @@ jobs: with: python-version: "3.12" + # Node is needed so the CI-parity step can regenerate the frontend API + # types (web/src/lib/api.gen.ts) that ci.yml's gen:api:check compares — + # the build agent's Python-only environment can't, which is why openapi + # changes kept landing frontend-codegen drift. + - name: Set up Node 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: web/package-lock.json + - name: Install Poetry run: pipx install poetry==1.8.3 @@ -102,9 +118,49 @@ jobs: - Follow existing patterns; tests use `fastapi.testclient.TestClient` over `bhulan.api.app:app` (public /v1 surface needs no MongoDB). - Record any material design choice as a new file in `spec/adrs/`. + - CI (which runs on the PR this loop opens) enforces MORE than the + test suite. Before you finish: run `poetry run ruff check bhulan/ + scripts/ tests/` and `poetry run mypy bhulan/` and FIX every error + you introduce (a later step auto-fixes import order, so focus on + real lint/type issues like unused names, naming, and Optional + narrowing). If you changed any request/response model, run + `poetry run python scripts/export_openapi.py` so `openapi.json` + stays in sync. Leaving any of these red ships a red PR that turns + master red when a human merges it. ${{ inputs.focus && format('Cycle note: {0}', inputs.focus) || '' }} claude_args: '--model opus --max-turns 45 --allowedTools "Read,Grep,Glob,Edit,Write,MultiEdit,Bash"' + # --------------------------------------------------------------------- + # CI PARITY — the loop's regression gate below is pytest-only, but ci.yml + # (which runs on the PR this loop opens) also enforces ruff, mypy, an + # openapi.json drift check, and the frontend gen:api:check. Without this, + # the build agent's commits land a PR that is RED on those extra gates, + # and merging it turns master red every cycle. Deterministically fix the + # MECHANICAL gates here — ruff import-order/unused autofix, and + # regenerating the two generated files — so only genuine mypy / lint + # errors remain, which are the build agent's job (see its HARD RULES). + # Non-fatal by design: the loop must still open a PR for human review; + # any gate left red is surfaced as a ::warning and by ci.yml on the PR. + # --------------------------------------------------------------------- + - name: CI parity — autofix lint + regenerate generated files + run: | + set +e + # Mechanical: import order + unused imports (the recurring I001/F401). + poetry run ruff check --fix bhulan/ scripts/ tests/ || true + # Mechanical: the OpenAPI snapshot ci.yml diff-checks. + poetry run python scripts/export_openapi.py \ + || echo "::warning ::openapi export failed — schema drift may remain on the PR" + # Mechanical: the frontend API types ci.yml's gen:api:check compares. + if [ -f web/package-lock.json ]; then + ( cd web && npm ci --no-audit --no-fund && npm run gen:api ) \ + || echo "::warning ::frontend api.gen.ts regen failed — codegen drift may remain on the PR" + fi + # Surface the gates that still need the build agent (or a human) to fix. + echo "== ruff (post-autofix) ==" + poetry run ruff check bhulan/ scripts/ tests/ || echo "::warning ::ruff still failing — the build agent must fix these" + echo "== mypy ==" + poetry run mypy bhulan/ || echo "::warning ::mypy still failing — the build agent must fix these" + # --------------------------------------------------------------------- # EVAL — the referee. Existing unit+integration suite = regression gate. # (tests/adversary/ holds the intentionally-failing acceptance/backlog diff --git a/reports/adversary/coverage-gaps.md b/reports/adversary/coverage-gaps.md index 8007542..96c2b1b 100644 --- a/reports/adversary/coverage-gaps.md +++ b/reports/adversary/coverage-gaps.md @@ -1,17 +1,17 @@ # Adversary coverage gaps -Product code reached by the adversary suite: **53.7%** +Product code reached by the adversary suite: **56.3%** Lines below have NEVER been executed by any adversary test — undiscovered defects live here. Targeting aid, not a KPI. | product file | unprobed lines | reached | |---|---|---| -| `bhulan/analytics/parsers.py` | 132 (`38,41-42,49,51-55,59,63-65,67-71,87,89,92-93,101,105,115,117,119,133-134,139,152-165,170-1`) | 45% | +| `bhulan/analytics/parsers.py` | 134 (`56,58,141,144-145,152,154-158,162,166-168,170-174,190,192,195-196,204,208,218,220,222,236-`) | 55% | | `bhulan/ingestion/files.py` | 107 (`7-11,13,15-19,22,24,27,40-41,43,53-54,56,59,70-72,75,85-87,89-91,93-97,100,111,113-116,119`) | 0% | -| `bhulan/analytics/file_parsers.py` | 105 (`48-49,53-56,58,60-63,65-66,70,74,77,81,91-92,94-96,123-124,130-132,147,150,160-178,195,199`) | 42% | +| `bhulan/analytics/file_parsers.py` | 100 (`48-49,53-56,58,60-63,65-66,70,74,77,81,91-92,94-96,123-124,130-132,147,150,160-178,195,199`) | 45% | +| `bhulan/api/app.py` | 94 (`74-75,93-94,120,130,191,198,216-217,222-223,242,248,268-269,275-276,285-286,295-302,304-31`) | 56% | | `bhulan/analytics/geocoding.py` | 91 (`22,24-27,29,35,37-39,41-43,46-49,52,60-63,65-66,68,70-76,78-79,81-83,89,92,95,98,105,111-1`) | 0% | | `bhulan/auth/service.py` | 88 (`44,57-58,64,90-95,100,111-114,119-124,127,133-134,138-139,143,145,152-154,160,166-171,178-`) | 32% | -| `bhulan/api/app.py` | 87 (`49,130-131,136-137,156,162,182-183,189-190,199-200,209-216,218-229,231,246-250,260,291-292`) | 49% | | `bhulan/ingestion/mqtt_consumer.py` | 86 (`7-11,13,15-18,20,23,26,42-45,47-48,50,52,54-57,59,61,63-65,67,69,71-72,74,76-77,79-82,84,8`) | 0% | | `bhulan/ingestion/normalize.py` | 73 (`47-50,64,71-73,75,89,95-97,99,113,115-117,119-121,123-126,128-131,133,135,159,161,163,165-`) | 13% | | `bhulan/ingestion/validate.py` | 64 (`28,30-32,34-35,49-50,52-53,55,68-70,72-73,75-76,78,91-92,94-95,97-98,100,113-114,116-117,1`) | 17% | @@ -20,14 +20,14 @@ Lines below have NEVER been executed by any adversary test — undiscovered defe | `bhulan/core/logging.py` | 40 (`7-10,13,16,26,36-47,49-50,52,55,63-64,66-67,69-70,73-74,76,80-81,83,85-87,90,100`) | 0% | | `bhulan/auth/db.py` | 29 (`71-73,78-83,86-89,91-92,98-106,108,113-115,117`) | 33% | | `bhulan/api/routes/auth.py` | 28 (`40-41,48,96-97,99-101,103,108,110,124-125,144-145,147-148,153-154,156,172-173,176-180,194`) | 63% | -| `bhulan/api/routes/insights.py` | 25 (`71,74,85,96,111,121,132,165,172-173,182-183,185,220,227,231-232,237,273,280-284,287`) | 78% | +| `bhulan/api/routes/insights.py` | 23 (`71,74,85,96,111,121,132,165,172-173,182-183,185,220,227,237,273,280-284,287`) | 80% | | `bhulan/api/deps.py` | 22 (`18-23,34-35,39-41,46-48,53,70-76`) | 35% | | `bhulan/auth/email.py` | 18 (`33-34,54-56,58-62,69-76`) | 49% | | `bhulan/api/routes/history.py` | 18 (`22-23,49-52,80-82,85-87,100-105`) | 65% | | `bhulan/models/canonical.py` | 15 (`39-44,50-52,61-62,66,74-76`) | 67% | | `bhulan/api/routes/compare.py` | 15 (`76,78,111,120,133-134,138-139,146,171,186-187,222,224-225`) | 82% | -| `bhulan/analytics/insights.py` | 15 (`166,374-375,378,385-390,394,398,411-412,415`) | 91% | +| `bhulan/analytics/insights.py` | 15 (`182,390-391,394,401-406,410,414,427-428,431`) | 92% | | `bhulan/models/vendor/generic.py` | 14 (`29,75,77,95-98,100,102-106,108`) | 22% | -| `bhulan/analytics/mobility.py` | 9 (`45,47,81,107,129,164,224,227,236`) | 93% | +| `bhulan/analytics/trips.py` | 9 (`77,96-98,131,142,152,174,188`) | 90% | +| `bhulan/analytics/mobility.py` | 9 (`45,47,81,131,153,188,260,263,272`) | 94% | | `bhulan/storage/base.py` | 8 (`27,40,45,65,78,99,118,131`) | 72% | -| `bhulan/api/limiter.py` | 8 (`27,57-61,65-66`) | 65% |