Skip to content

chore(devex): lint cyclomatic complexity of changed files - #91305

Merged
trunk-io[bot] merged 11 commits into
masterfrom
feat/cyclomatic-complexity-lint
Aug 31, 2026
Merged

chore(devex): lint cyclomatic complexity of changed files#91305
trunk-io[bot] merged 11 commits into
masterfrom
feat/cyclomatic-complexity-lint

Conversation

@pauldambra

@pauldambra pauldambra commented Aug 29, 2026

Copy link
Copy Markdown
Member

it really stuck with me when Tom O said (roughly) "agents can template warehouse sources cos the code is really clean, so it's easy to copy"

but in general at PostHog we've not bothered too much about "clean code" signals like file/module size or code complexity. leaning on "high talent density" to get by... but agents are not talented humans so...

Problem

  • Functions keep landing with cyclomatic complexity beyond what a reviewer can hold in their head, and nothing in review or CI flags them.
  • Neither linter can simply be switched on: ruff's C901 is ignored repo-wide (~527 pre-existing violations) and oxlint has no complexity rule.
  • An earlier iteration failed CI above complexity 15, and batched PRs that only touched pre-existing complex files kept failing. Enforcement needs to start as signal, not a gate.

Changes

  • Changed files get a cyclomatic complexity check where every finding above 10 is a warning. Nothing fails CI yet - the check starts as signal while the pre-existing backlog settles.
  • Warnings show up in two places a reviewer actually looks: annotations on the PR diff, and a "Complexity (Python)" / "Complexity (TypeScript)" section in the shared CI report comment on the PR. Clean runs post an ok section so stale warnings disappear on the next push.
  • hogli lint:complexity runs the same check on demand: changed files by default, explicit paths, or --against <ref>. It exits 0 even with findings.
  • Python goes through ruff's C901 (cleared of its repo-wide ignore only for the checked files); TypeScript gets bin/lint-complexity.mjs on the TypeScript compiler API, since oxlint has no complexity rule. A test pins the thresholds and the --report contract between the two sides.
  • hogli ci:preflight shows the findings as a non-blocking ⚠ warning.
  • The CI steps read changed files from paths-filter (PR files API), so content merged in from master is never blamed on the PR.
  • Mechanical: hogli.yaml command wiring, the running-ci-preflight skill note, and a new .github/scripts/post-complexity-section.mjs writer for the CI report.

Before

flowchart LR
    D{{PR diff}} --> R[ruff check, repo-wide]
    D --> O[oxlint, repo-wide]
    R --> G[CI verdict]
    O --> G
    classDef phBlue fill:#1d4aff,color:#fff,stroke:#1d4aff
    classDef phYellow fill:#f9bd2b,color:#000,stroke:#f9bd2b
    class R,O phBlue
    class D,G phYellow
Loading

After

flowchart LR
    D{{PR diff}} --> C[complexity check, changed files only]
    C --> W[warnings: diff annotations + CI report sections]
    D --> R[ruff check, repo-wide]
    D --> O[oxlint, repo-wide]
    R --> G[CI verdict]
    O --> G
    classDef phBlue fill:#1d4aff,color:#fff,stroke:#1d4aff
    classDef phYellow fill:#f9bd2b,color:#000,stroke:#f9bd2b
    class C,R,O phBlue
    class W,D,G phYellow
Loading

How did you test this code?

  • tools/hogli-commands/hogli_commands/tests/test_complexity_lint.py:
    • Real ruff runs over a fixture, parameterized at the boundary: complexity 10 not reported, 12 and 17 both reported as warnings (no error tier).
    • CLI test: a complexity-17 finding still exits 0 and lands in the --report JSON file the CI poster reads.
    • Scope test: out-of-scope and deleted paths never reach ruff.
    • Threshold and --report contract of the mjs pinned to the Python side.
    • Preflight renders the soft warning without blocking --strict.
  • frontend/bin/ci-report jest tests pass with the two new section ids registered.
  • Manual: hogli lint:complexity on frontend/src/scenes/data-warehouse/editor/OutputPane.tsx (the file that failed CI in batched PRs) now prints 5 warnings and exits 0; hogli lint:workflows and mypy on the hogli package pass.
  • Not run: the two CI steps and the comment posting end to end - the first execution is this PR's own CI.

Automatic notifications

  • Publish to changelog?

Docs update

The running-ci-preflight skill documents the warning-only behavior in this PR.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Built with Claude Code; later iterations in PostHog Desktop task sessions. Repo skills consulted: authoring-ci-workflows, writing-tests, writing-pr-descriptions, running-ci-preflight.
  • Session decisions: the error tier shipped first and batched PRs kept tripping it on pre-existing files, so the check now starts as warnings only - findings annotate the diff and land in the shared CI report comment (one section per language) instead of failing jobs. Both linters gained a --report flag so CI steps hand findings to the report poster as JSON.
  • eslint is no longer a dependency and oxlint has no complexity rule, so the TS checker uses the already-shipped typescript package.

Created with PostHog Desktop

Complexity 11-15 warns, above 15 fails — in hogli lint:complexity,
ci:preflight, and CI (ci-python, ci-frontend). Scoped to changed files
under posthog/, ee/, products/, and frontend/. Python via ruff C901,
TypeScript via a small checker on the TS compiler API (oxlint has no
complexity rule).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pauldambra pauldambra self-assigned this Aug 29, 2026
@trunk-io

trunk-io Bot commented Aug 29, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

🚨 Trunk lane — universal lane

This PR is assigned to the universal lane. It cannot merge in parallel with other PRs, so it can take longer to merge. Ask dev-ex if you think this is wrong.

Complexity (TypeScript) — clean

Cyclomatic complexity above 10 in changed typescript files. Warn only: worth simplifying when you next touch these functions.

Bundle size — no change

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 68.04 MiB · no change

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.41 MiB · 22 files no change ███░░░░░░░ 31.3% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.83 MiB · 3,245 files no change █████████░ 90.9% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
307.0 KiB ../node_modules/.pnpm/posthog-js@1.422.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
263.5 KiB ../node_modules/.pnpm/posthog-js@1.422.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.js
252.9 KiB src/taxonomy/core-filter-definitions-by-group.json
154.2 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
104.7 KiB src/lib/api.ts
95.2 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Toolbar bundle — eager 2.26 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.26 MiB · 18 files no change ████░░░░░░ 39.5% of 5.72 MiB
Deferred (lazy) 2.11 MiB · 45 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
750.7 KiB dist/toolbar/toolbar-app-HC2BLVR5.css
588.3 KiB dist/toolbar/chunk-chunk-UZOF23P7.js
484.7 KiB dist/toolbar/chunk-chunk-OQ4CRAT2.js
134.1 KiB dist/toolbar/chunk-chunk-ESP3L22S.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
71.3 KiB dist/toolbar/toolbar-app-PMPWTAOL.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-XNAOAA7H.js
21.0 KiB dist/toolbar/chunk-chunk-U6ERKZIE.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

Dist folder size — no change

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1443.06 MiB · no change

Playwright — all passed

All tests passed.

View test results →

@trunk-io

trunk-io Bot commented Aug 29, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@pauldambra
pauldambra marked this pull request as ready for review August 29, 2026 18:57
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T19:01:45.574158Z e5ff46c Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team August 29, 2026 18:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5ff46c8ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci-frontend.yml Outdated
Comment thread tools/hogli-commands/hogli_commands/complexity_lint.py Outdated

@pauldambra pauldambra left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated comment by QA Swarm — not written by a human

QA Swarm review complete. See inline comments.

Comment thread bin/lint-complexity.mjs
Comment thread tools/hogli-commands/hogli_commands/complexity_lint.py Outdated
Comment thread tools/hogli-commands/hogli_commands/tests/test_complexity_lint.py Outdated
@pauldambra

pauldambra commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review: router (cheap-first pass) + delegated reviewers (qa-team, paul-reviewer, xp-reviewer, security-audit as warranted)

Verdict: ✅ APPROVE (round 3 @ f3c1657)

Round 3 reviewed the switch of both CI complexity steps from hand-rolled git fetch-and-diff to the changes job's paths-filter file list (list-files: escape, consumed with the ci-backend.yml printf|xargs precedent). Wiring, if-conditions, and globs all check out. Danger LOW, confidence HIGH, no delegation.

Key findings

None this round.

Convergence

None — single-reviewer round (router did not delegate).

Reviewer summaries

Reviewer Assessment
🧭 router Verified the paths-filter wiring against the ci-backend precedent; danger LOW, confidence HIGH, delegated nothing
Previous rounds (2)

round 1 @ e5ff46c — ✅ APPROVE: implementation verified empirically; 2 LOW + 1 NIT findings, all since fixed and resolved.
round 2 @ 3f0ffc2 — ✅ APPROVE: fix commits re-reviewed, no regressions; 1 pre-existing NIT (no JS test harness for the mjs).


Automated by QA Swarm — not a human review

pauldambra and others added 5 commits August 29, 2026 20:13
Open PRs that haven't rebased onto this commit lack bin/lint-complexity.mjs
and the lint:complexity hogli command, so the new CI steps would fail before
their existing checks finish. Skip each step gracefully when its companion
tooling isn't present yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- resolve class-field arrow function names (e.g. handleClick = () => {})
  instead of falling through to <anonymous>
- only count files toward "checked" when their checker actually ran, and
  surface a degraded run to stdout so a soft preflight check reports it as
  a warning instead of a silent pass

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hecked

The out-of-scope fixture path didn't exist, so it was filtered by the
earlier is_file() check rather than by matches_globs against PYTHON_SCOPE,
which the test comment claims to cover. manage.py exists and is genuinely
out of scope, so it exercises the intended branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
github.event.pull_request.base.sha can lag the merge ref GitHub rebuilds
against current master, so a two-dot diff against it blamed files master
changed after the event fired. HEAD^1 is the exact base snapshot the
checkout contains, so the diff can never include master's own commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pauldambra pauldambra added the stamphog Request AI approval (no full review) label Aug 29, 2026
@stamphog

stamphog Bot commented Aug 29, 2026

Copy link
Copy Markdown

Gates denied on CI workflow changes (deny-listed infra_cicd) and cross-cutting size; this is risky territory (modifies GitHub Actions CI jobs) and the author is not on the owning devex team, with no independent review verified against the current head commit.

  • Deny-list match: touches .github/workflows/ci-frontend.yml and ci-python.yml (infra_cicd)
  • Classified T2-never due to cross-cutting scope across 8 files
  • Author pauldambra is not on the owning @PostHog/team-devex team
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list matches: infra_cicd
size 386L, 6F substantive, 476L/8F incl. docs/generated/snapshots — within ceiling
tier classified as T2-never: T2-never (476L, 8F, cross-cutting, chore)
stamphog 2.0.0b4 .stamphog/policy.yml @ f8ef564 · reviewed head f8ef564

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Aug 29, 2026
The hand-rolled fetch-and-diff snippets already misfired once when the
event's base sha lagged the rebuilt merge ref. paths-filter reads the PR
files API, which is authoritative for what the PR changed, so the steps
now consume its escaped file list instead of any git plumbing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…CI report

Batched PRs kept failing CI on the error tier for pre-existing complex
files they only touched. Drop the error tier: every finding above the
threshold is now a warning, and the check never fails a job or preflight.

Findings now surface in the grouped CI report comment (one section per
language) alongside the diff annotations, so reviewers see them without
opening job logs. Both linters gained a --report flag that writes the
findings as JSON for the new .github/scripts/post-complexity-section.mjs
poster; clean runs post an ok section so stale warnings clear on the next
push.

Generated-By: PostHog Desktop
Task-Id: 3a98bf2c-534d-40b2-bbd7-b34eb51e7afd
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ❌ build failed

The preview didn't come up for commit 9e5fad2. See the build log for the failing step. It'll retry on the next push.

Previews are optional and never block merging. A failure here is often a hogland or tailnet hiccup rather than anything in your PR, so the check stays green and this comment is the status.

Comment thread .github/scripts/post-complexity-section.mjs
File paths and function names in the findings come from PR-authored
files, and git allows filenames containing backticks, pipes, and
newlines. Interpolated into the CI report comment raw, a filename could
break out of the table or forge section markers that the report parser
trusts as metadata on the next write.

Strip control characters, backticks, and pipes from interpolated cells
via a shared markdownCell helper in the ci-report formatter, with tests
covering the forged-marker case.

Generated-By: PostHog Desktop
Task-Id: 3a98bf2c-534d-40b2-bbd7-b34eb51e7afd
Generated-By: PostHog Desktop
Task-Id: 3a98bf2c-534d-40b2-bbd7-b34eb51e7afd
@pauldambra pauldambra added the stamphog Request AI approval (no full review) label Aug 31, 2026 — with PostHog
@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Aug 31, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved yet — waiting on the conditions below.

Re-add the stamphog label to request another review once you have addressed this.

Gates denied this PR (CI/workflow files touched, classified as never-auto tier), and the change is genuine infra/CI-pipeline territory — new GitHub Actions steps and workflow wiring, not incidental. The only "approvals" are on stale commits (gantoine, Codex) or self-posted by the author's own account (QA Swarm), so there's no independent assurance on the current head, and the author isn't on the owning devex team.

  • Gate verdict DENIED (deny-list: infra_cicd, tier: T2-never) — cannot auto-approve regardless of content
  • Modifies .github/workflows/ci-frontend.yml and ci-python.yml plus adds a new CI report-posting script — genuine CI/deploy tooling change
  • Author pauldambra is not on the owning @PostHog/team-devex team
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list matches: infra_cicd
size 485L, 9F substantive, 611L/12F incl. docs/generated/snapshots — within ceiling
tier classified as T2-never: T2-never (611L, 12F, cross-cutting, chore)
stamphog 2.0.0b4 .stamphog/policy.yml @ 9e5fad2 · reviewed head 9e5fad2

@trunk-io
trunk-io Bot merged commit 947f318 into master Aug 31, 2026
251 of 258 checks passed
@trunk-io
trunk-io Bot deleted the feat/cyclomatic-complexity-lint branch August 31, 2026 23:05
@deployment-status-posthog

deployment-status-posthog Bot commented Aug 31, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-08-31 23:29 UTC Run
prod-us ✅ Deployed 2026-08-31 23:48 UTC Run
prod-eu ✅ Deployed 2026-08-31 23:51 UTC Run

dmarticus added a commit that referenced this pull request Sep 4, 2026
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
dmarticus added a commit that referenced this pull request Sep 4, 2026
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
dmarticus added a commit that referenced this pull request Sep 4, 2026
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants