Skip to content

ci: enforce comment-encoding rules with a guard - #1926

Merged
lmeyerov merged 3 commits into
masterfrom
ci/comment-density-guard
Aug 15, 2026
Merged

ci: enforce comment-encoding rules with a guard#1926
lmeyerov merged 3 commits into
masterfrom
ci/comment-density-guard

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

Standalone off master. Root cause of the class you rejected four PRs running: comment discipline was the only rule in this repo with no automated gate. Type hygiene, cypher surface, coverage floors and the polars lane-completeness lock all have one; comments were enforced by human review alone, so they were the only class that reliably reached you.

bin/ci_comment_density_guard.py — stdlib tokenize+ast, modelled on ci_type_hygiene_guard.py: same per-file ratchet, same --report/--list/--update-baseline/--strict surface, same exit codes, ~3s, identical counts on py3.10 and py3.12. Escape hatch # guard-ok: <check> -- <reason>.

Checks: comment-block (2+ adjacent full-line #; tool directives and the file header neither start nor continue a run; a #: Sphinx run is allowed at 2 and flagged at 3+ — the loophole you called out), perf-claim (O(...) plus faster/slower/vectoriz/measurably/cheap/expensive/hot-path), issue-rationale (a standalone comment or docstring citing #NNN; a trailing # #1891 on a code line is fine).

Acceptance test: 16/16 of your flagged comments

Replayed against the exact commits you reviewed (9e16c74, b645af6, d044e3e; the outdated anchors resolved via original_commit_id). Every one is caught — the query_types.py note, all four "eliminate/shorten" sites, the three lowering.py "should this be a test?" sites, both agg_types.py sites (the second via the #: 3+ rule), the polars chain and hop_eager sites, hop.py, and execution.py.

More decisive than flagging: the ratchet would have failed all three PRs — all 9 touched files exceed their master cap (e.g. gfql_unified.py 32/1/15 → 36/1/18, hop.py 6/4/1 → 10/6/6, query_types.py absent from baseline → 1).

Two deviations from my brief, both forced by that acceptance test

  1. comment-block is not restricted to function bodies — query_types.py:9-12 and agg_types.py:53-55 are module-scope inside if TYPE_CHECKING:, and you flagged both.
  2. perf-claim/issue-rationale also read docstrings — a claim doesn't become admissible by moving into a docstring, and flatten.py's 22-line narration is a docstring.

False positives: measured, removed, and pinned as tests

Raw perf-claim was 344. Two classes were noise: regress* (101 hits) was almost all correctness — "REGRESSION GUARD — DO NOT REMOVE", plus junk from regressive — and A/B (6) matched a/b/c id columns. Both now require performance vocabulary in the same comment. A comment naming pyg-bench is exempt from the benchmark term: pointing at where the measurement lives is the correct encoding. Net 344 → 216, no loss on the acceptance set. These decisions are locked in tests, not prose — 22 tests in test_ci_comment_density_guard.py; neither existing guard has one.

Master's grandfathered debt: comment-block 948/138 files, perf-claim 216/88, issue-rationale 169/50. The guard also cleans its own five 2-line comments, so it obeys itself.

Honest limits — this is ~two-thirds of the class

It closes comment volume, which is the dominant violation and purely structural. It cannot decide whether a one-line comment earns its place (that is "could a test have been written instead?" — undecidable), and the predictable evasion is compressing narration into one dense 120-char line. It catches a docstring citing an issue or claiming perf, not one narrating mechanism — a length rule was considered and rejected as ~90% noise against 254 legitimate long docstrings. And "should this be a symbol / externed DRY?" is not mechanizable at all.

It also stays a ratchet: lowering.py already carries a cap of 50, and within a cap comments can be freely rewritten. --strict would tighten that over time but fails today.

So it stops the specific thing that reached you four times and shrinks the residue to a review conversation — it does not make the review skill unnecessary.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi

The "Encoding: names, tests, and structure -- not prose" rules in
agents/skills/review/SKILL.md were the only rule class on the 2026-08 GFQL
stack with no automated enforcement, which is exactly why they were the only
class that kept reaching the owner: rejected on #1894, #1895 and #1897 for the
same reason each time. Everything else that mattered already had a gate --
bin/ci_type_hygiene_guard.py (AST, per-file baseline), bin/ci_cypher_surface_guard.py
(line-count ratchet), the per-file coverage floors, test_polars_lane_completeness.py.
Comment discipline was left to human review. This converts it into a gate.

bin/ci_comment_density_guard.py is a stdlib-only tokenize + ast pass over
graphistry/, modelled on the type-hygiene guard: same per-file count ratchet,
same --report/--list/--update-baseline/--strict surface, same exit codes, and a
`# guard-ok: <check> -- <reason>` escape hatch. It runs from bin/lint.sh, so it
shares the python-lint-types matrix (py3.8-3.14) with no new workflow and no
new job; that lane fires on any *.py change.

Checks:
  comment-block     2+ adjacent full-line `#` comments (3+ for a Sphinx `#:`
                    run, which was being used as a prose loophole)
  perf-claim        complexity notation or performance vocabulary
  issue-rationale   a standalone comment or docstring citing an issue number
                    as the explanation

comment-block is a form rule and reads `#` comments only; the two content rules
also read docstrings, since a claim does not become admissible by moving into
one. Tests are exempt from comment-block and issue-rationale -- a test may
explain its oracle -- but not from perf-claim: measurement belongs in pyg-bench
wherever it is written.

Master's grandfathered debt: 948 comment-block, 216 perf-claim, 169
issue-rationale.

Acceptance was measured, not asserted. Replayed against the three rejected
branches at the commits the owner reviewed, the guard flags all 16 sites he
flagged by hand, and each of the 9 touched files exceeds its master cap, so the
ratchet would have failed all three PRs. Two false-positive classes were
measured and tuned out rather than left as noise: `regress`/`A/B` also name
correctness concepts, so they count only next to performance vocabulary; and a
comment naming pyg-bench is a pointer to where measurement lives, not a claim.
Together those were 128 of 344 raw perf-claim hits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Self-audit against the rules this guard enforces: the file carried six
single-line rationale comments explaining why each pattern is tuned the way it
is. Five were already stated verbatim by a test name, so they were pure
duplication:

  pyg-bench claim applies to tests too ->
    test_tests_are_exempt_from_comment_block_but_not_from_perf_claim
  tool directives never start/continue a run ->
    test_tool_directives_neither_start_nor_continue_a_run
  Sphinx wraps at two, prose at three ->
    test_sphinx_run_is_allowed_at_two_lines_and_flagged_at_three
  regress/A-B need perf context ->
    test_correctness_regression_without_performance_context_is_not_a_perf_claim
    test_regression_next_to_performance_vocabulary_is_a_perf_claim
  pyg-bench is a pointer, not a claim ->
    test_pointing_at_pyg_bench_is_not_a_perf_claim

The sixth -- complexity notation matching case-sensitively so `foo(` and
`into(` cannot hit -- had NO test. It was a real coverage gap that a comment
was standing in for, which is exactly the failure mode the guard exists to
stop. Now pinned by test_a_lowercase_call_is_not_asymptotic_notation
(foo(x), into(rows), o(n), do(work)) and its positive twin
test_uppercase_complexity_notation_is_asymptotic_notation.

Both pins verified by MUTATION: adding re.IGNORECASE to the O( pattern turns
o(n) red, then reverted. The first mutation attempt was a sed that silently
did not match, so the pass was vacuous -- re-done and confirmed failing.

Note the blind spot this exposes: these six comments passed the guard because
`comment-block` only fires on runs of 2+ adjacent comment lines. A one-line
rationale comment is still rationale, and the guard does not see it. Raising
that would reclassify a large share of the 948 grandfathered comment-block
findings, so it is flagged for the owner rather than changed here.

27 guard tests pass; guard clean on the repo; ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Comment thread .github/workflows/ci.yml Outdated
'^bin/ci_type_hygiene_guard\.py$' \
'^bin/ci_type_hygiene_baseline\.json$' \
'^bin/ci_comment_density_guard\.py$' \
'^bin/ci_comment_density_baseline\.json$'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Better to figure out a refactor for an encapsulated folder include?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 38162c5b3 — five lines become two.

Two of the four entries I added were dead. The group's first pattern is '\.py$', unanchored, so bin/ci_type_hygiene_guard.py and bin/ci_comment_density_guard.py were already matched. Listing them changed nothing. Only the two .json baselines ever needed covering.

So the encapsulation is one pattern:

'^bin/ci_.*baseline\.json$'

A future guard now needs no ci.yml edit at all — its script is caught by \.py$ and its baseline by the glob. Verified both directions: matches all four guard inputs plus ci_cypher_surface_guard_baseline.json, and does not match docker/, README.md, bin/something.sh or requirements/*.lock.

Pinned rather than described, by test_every_guard_baseline_triggers_the_python_ci_lane — it parses the live workflow and asserts every bin/ci_*baseline*.json on disk is matched by the python group. Mutation-verified: narrowing the pattern back to a single baseline turns it red. That is the part no comment could carry, because the failure mode is a guard added later whose baseline silently stops triggering the lane.

On the literal folder include (bin/ci_guards/): considered, and deferred rather than dismissed. 10 of the 13 open stack PRs modify a guard baseline, so renaming those files right now would conflict with every one of them — and unwinding exactly that kind of cascade is what most of today went to. Filed for post-release with the other structural moves (alongside the .agents/ migration). The pattern above already removes the per-guard edit that prompted your comment, so the deferral costs nothing ongoing.

Review (r3790045223): the ci.yml `python` path-filter grew one regex per guard
file, so every new guard needed a workflow edit. Five lines become two.

Two of the four added entries were DEAD. The group's first pattern is `\.py$`,
unanchored, so `bin/ci_type_hygiene_guard.py` and
`bin/ci_comment_density_guard.py` were already matched -- listing them changed
nothing. Only the two `.json` baselines ever needed covering, and they are now
covered by one `^bin/ci_.*baseline\.json$`, so a future guard needs no ci.yml
edit at all. Verified positively and negatively: it matches all four guard
inputs plus the cypher-surface baseline, and does not match docker/, README.md,
bin/something.sh or requirements/*.lock.

Pinned rather than described, by
`test_every_guard_baseline_triggers_the_python_ci_lane`, which parses the live
workflow and asserts every `bin/ci_*baseline*.json` on disk is matched by the
python group. Mutation-verified: narrowing the pattern back to a single
baseline turns it red. That is the part a comment could not carry -- the
failure mode is a NEW guard added later whose baseline silently stops
triggering the lane.

An actual folder move (`bin/ci_guards/`) was considered and DEFERRED, not
dismissed: 10 of the 13 open stack PRs modify a guard baseline, so renaming
those files now would conflict with every one of them. Filed for post-release
alongside the other structural moves; the pattern above already removes the
per-guard edit that prompted the comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
@lmeyerov
lmeyerov merged commit 8c32785 into master Aug 15, 2026
79 checks passed
@lmeyerov
lmeyerov deleted the ci/comment-density-guard branch August 15, 2026 20:52
lmeyerov added a commit that referenced this pull request Aug 15, 2026
#1895 went CONFLICTING when #1894 landed on master. One conflict, in
polars/chain.py: master added `edge_src, edge_dst = _bound_edge_endpoints(self)`
while this branch trimmed the perf claims out of the comment below it. Kept
master's code AND this branch's trimmed comment.

The merge also brought in #1926's comment-density guard, which then FAILED:
hop.py carried 8 comment-block findings against a cap of 6. Master's own six
are the same six, displaced by ~14 lines; the two extra were added by this
branch, at the endpoint-backfill epilogue and at the seed-label writer.

Both narrated the same contract -- the hop-label column used to ride the
endpoint concat as a side effect, that concat is now conditional, and `.loc`
recreates the column implicitly on pandas but RAISES on cudf. That is a rule,
so it is now a NAME: `_nodes_with_hop_label_column`, called at both sites, with
the nested `_ensure_node_hop_col` reduced to one call.

I wrote two pins for it and then DELETED them: they passed under mutation, so
they were decorative. Mutating the helper to a no-op is caught by four pins that
already exist -- `test_output_hop_window_slices_only_closed_edges[...cudf]` and
`test_output_window_on_a_cycle_keeps_only_the_windowed_endpoints` -- including a
cudf lane, which is the engine that actually crashes. The contract was already
covered; the comments were the redundant part, not the tests.

One of the deleted pins had also encoded a WRONG oracle: it assumed every engine
serves `label_node_hops` with `min_hops`, but polars declines that combination
with a typed NotImplementedError naming both features. Legitimate decline, wrong
expectation on my side.

262 passed in test_endpoint_closure_matrix.py; ruff clean; all three guards pass
(comment-density now 28 files below baseline, type-hygiene 1 below; neither cap
raised).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
lmeyerov added a commit that referenced this pull request Aug 15, 2026
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.

1 participant