Skip to content

fix: make local semantic-line-breaks consume CI's NLB checker (closes #2085) - #2322

Merged
d-morrison merged 12 commits into
mainfrom
cursor/nlb-local-ci-agree-26b4
Aug 26, 2026
Merged

fix: make local semantic-line-breaks consume CI's NLB checker (closes #2085)#2322
d-morrison merged 12 commits into
mainfrom
cursor/nlb-local-ci-agree-26b4

Conversation

@dem-extra1

@dem-extra1 dem-extra1 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Closes #2085.

The local reformatter (scripts/semantic-line-breaks.py) and CI's check-new-line-breaks job disagreed on mid-line semicolons (and on lowercase-follower sentence boundaries). --write could therefore produce a CI failure. Measured on #2073 at e88f2eec and again on #2092.

Construction

Agreement is by construction, not by two implementations happening to match:

  • .github/workflows/validate.yml already pins Morrison-Lab/gha/check-new-line-breaks@fdaeccc47273fb8c542d05b00d488242b8563b15.
  • scripts/vendor/gha-check-new-line-breaks.py is that script at that SHA (byte-identical; do not hand-edit).
  • scripts/vendor/gha-check-new-line-breaks.pin records the git SHA and the sha256 of those bytes.
  • scripts/lib/nlb_gate.py refuses to load if the pin disagrees with validate.yml or the file bytes.
  • The reformatter emits through emit_gate_clean: sentence splits come from the gate's split_sentences; clause splits run only when classify_line returns clause.

Refresh after an action-pin bump: python3 scripts/sync-nlb-checker.py.

Predicates checked (not only the semicolon)

Against the vendored checker at that SHA, the local splitter also missed _SENT_BREAK_LOWER_RE (a period after two lowercase letters, then a lowercase word). That path is now the gate's function, so --write splits it and no longer rejoins a hand-break. Extra lowercase abbrevs (min/hr/hrs) ride along on that same branch.

#2081 left alone

The gate does not flag comma clauses (gha#336). A long comma-clause sentence stays one line; scripts/test_slb.py pins that. Closing the 200-400 character comma-join trap is a different issue.

Tests

python3 scripts/test_slb.py: 79 passed. Covers the pin, the #2085 semicolon line, lowercase-follower splits, code-span masking on a line long enough to be a clause, ellipsis-before-capital, and that #2081 comma joins are unchanged.

Open in Web Open in Cursor 

cursoragent and others added 7 commits August 26, 2026 17:03
WIP --- opened up front to claim the issue; implementing now.

Co-authored-by: dem-extra1 <dem-extra1@users.noreply.github.com>
The local reformatter and gha's check-new-line-breaks were two
implementations of one convention. A semicolon is not a sentence
boundary, so --write joined clause pairs onto one line and CI
rejected them.

Load the checker at the SHA validate.yml pins, split sentences with
its split_sentences, and split mid-line semicolons when its
classify_line returns clause. A pin file records that SHA and the
file's sha256 so a stale or hand-edited copy fails loudly.

#2081 (comma-clause joins into 200-400 char lines) is unchanged:
the gate does not flag commas.

Co-authored-by: dem-extra1 <dem-extra1@users.noreply.github.com>
Self-review of the #2085 construction found leftover Don'ts and a
local-run sentence that still described the pre-fix reformatter.
Date those case records, point skill-builder and
challenge-the-assignment at the new load path, and catalog the
vendored checker in AGENTS.md's generated-output table.

Co-authored-by: dem-extra1 <dem-extra1@users.noreply.github.com>
The ellipsis section still said the reformatter misses that boundary.
After #2085 it does not.

Co-authored-by: dem-extra1 <dem-extra1@users.noreply.github.com>
Past-tense leftover claims, an ambiguous "It" in skill-builder, and a
masking test that only passed because the fixture was too short for
classify_line to return clause.

Co-authored-by: dem-extra1 <dem-extra1@users.noreply.github.com>
…on't

`--write` already splits `[...] Text` the same way the checker does.
The scoped `find` fallback now calls emit_gate_clean too, so a miss cannot
write a line the gate would still flag.

Co-authored-by: dem-extra1 <dem-extra1@users.noreply.github.com>
The continuation packed "see it. It shares" onto one added line.

Co-authored-by: dem-extra1 <dem-extra1@users.noreply.github.com>
@dem-extra1
dem-extra1 marked this pull request as ready for review August 26, 2026 18:03
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@dem-extra1

Copy link
Copy Markdown
Collaborator Author

Session wrap 2026-08-26 12:31 PDT: the Ready on 271a6f4 still stands and mergeable_state was clean. The non-blocking _fetch FileNotFoundError finding was not Addressed this session (Cloud Agents blocked on Cursor monthly quota). Tracked as #2338. Do not merge from this comment.

Posted by Claude Code (AI agent) --- not written by a human.

…o nlb_gate

nlb_gate.py derived which checker script CI runs from validate.yml's
`uses:` pin, but called classify_line/split_sentences at each call
site's compiled-in default (clause_breaks=True, clause_min_length=80)
rather than validate.yml's `with:` block. Today those match by
coincidence; a future `with: clause-min-length: '10'` would make CI
flag lines the local reformatter leaves alone, with nothing to catch
the drift.

Add parse_ci_nlb_with/resolve_nlb_config/load_nlb_config, which parse
the NLB step's `with:` block and resolve clause-breaks/clause-min-length
through the vendored checker's own _env_flag/_env_int (round-tripped
through os.environ and restored), so a config value validate.yml sets
or omits resolves exactly as CI's NLB_CLAUSE_BREAKS/NLB_CLAUSE_MIN_LENGTH
would. emit_gate_clean now defaults to that resolution and threads it
through every recursive classify_line call.

Adds 7 tests to test_slb.py: the live-validate.yml resolution equals
today's defaults, and a synthetic clause-min-length: '10' validate.yml
changes both classify_line's verdict and emit_gate_clean's output for
the reviewer's example line. 86 passed, 0 failed (was 79).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MyNR6ZrvN6M4Rr8eZbHts

Copy link
Copy Markdown
Collaborator

Pushed 233eba9 addressing the adversarial finding raised against 271a6f4 (local/CI agreement was coincidental --- the gate pinned the checker's SHA but ignored the action's clause-config inputs):

Addressed. nlb_gate.py now parses validate.yml's NLB step with: block (clause-breaks/clause-min-length) via a new resolve_nlb_config(), resolving both through the vendored checker's own _env_flag/_env_int for exact CI parity, and emit_gate_clean defaults to and threads that resolution through every recursive classify_line call instead of the compiled-in defaults. Today's live config still resolves to (True, 80), and a synthetic-validate.yml test proves the reviewer's example line ("Short clause here; second bit.") flips from clean to a clause violation, and from left-whole to split, once clause-min-length drops to 10. The module docstring's "by construction" claim now states that config, not just script identity, is derived from validate.yml. 86/86 tests pass (79 prior + 7 new).

The one earlier finding (sync-nlb-checker.py FileNotFoundError without gh) remains Deferred to #2338, as before.

Posted by Claude Code (AI agent) --- not written by a human.


Generated by Claude Code

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

The review round on 233eba9 reproduced a regression: the config-
threading commit's module-scope yaml import made the previously
dependency-free reformatter crash with a raw traceback (even --help) on
machines without PyYAML. Move the import inside parse_ci_nlb_with,
guarded with the repo's friendly-exit convention (matching
validate-skills.py), so every yaml-free path works and a run that needs
the config exits with the install instruction. Two regression tests
block a shim yaml module and assert --help succeeds and the real run
gets the message, not a traceback.

88/88 tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MyNR6ZrvN6M4Rr8eZbHts

Copy link
Copy Markdown
Collaborator

Pushed 980922d addressing the round at 233eba9:

Addressed --- the unguarded module-scope import yaml is gone. The import now lives inside parse_ci_nlb_with, guarded with the repo's friendly-exit convention (same pattern as validate-skills.py), so every yaml-free path --- --help included --- works on a machine without PyYAML, and a run that genuinely needs CI-config resolution exits with the pip install pyyaml instruction instead of a raw traceback. Two regression tests inject a blocking yaml shim on PYTHONPATH and assert both directions. 88/88 tests pass.

Posted by Claude Code (AI agent) --- not written by a human.


Generated by Claude Code

claude added 2 commits August 26, 2026 22:24
The CI checkout cannot resolve origin/main, so the previous test file
hit the scope-diff error before the yaml path and failed on the message
assertion. Probe with --all on a temp file outside the repo instead,
which skips base-ref scoping and deterministically reaches the config
resolution that needs PyYAML. Reproduced the CI failure shape locally
before the fix; 88/88 after.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MyNR6ZrvN6M4Rr8eZbHts
The adversarial round measured a ~170x whole-file reformat regression:
emit_gate_clean resolved the checker singleton before calling
load_nlb_config, so the no-override cache condition never held and
validate.yml was re-parsed once per sentence. Pass the caller's checker
(None on the ordinary path) instead, and pin the behavior with a
counting test (resolution runs at most once across repeated calls).
Also cover the clause-breaks axis the round showed untested: a
synthetic clause-breaks 'false' config resolves and changes
classification, so silently dropping that axis now fails a test.

92/92 tests pass; whole-file preview timing back to sub-second.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MyNR6ZrvN6M4Rr8eZbHts

Copy link
Copy Markdown
Collaborator

Pushed 112dd70 addressing the adversarial round at 233eba9:

# Finding Disposition
1 Config cache never engaged on the real path (~170x measured whole-file regression --- validate.yml re-parsed per sentence) Addressed --- emit_gate_clean now hands load_nlb_config the caller's checker (None on the ordinary path), so the no-override cache holds; a counting test pins resolution to at most once across repeated calls; whole-file preview of CLAUDE.md measured back to 0.14s.
2 clause-breaks axis had no non-default regression coverage (dropping it passed all tests) Addressed --- synthetic clause-breaks: 'false' config test added: it resolves, flips classification of a long semicolon line, and contrasts against the live default.
3 Unguarded module-scope yaml import Addressed earlier in 980922d (lazy guarded import, friendly install message, shim-blocked regression tests) --- confirmed by that round's note that the head had advanced.

Now 92/92 tests. The interim CI failure at 980922d was the new PyYAML test depending on a git base ref CI cannot resolve; fixed in cf65603 (probe via --all on a temp file).

Posted by Claude Code (AI agent) --- not written by a human.


Generated by Claude Code

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

The delta adversarial round showed the guard exit escaped the
reformatter per-path except Exception loop (SystemExit is not an
Exception), so a multi-path run died at the first path with no summary,
and the comment claiming parity with the module-scope guard convention
overclaimed. The guard now raises NLBPinError, which the per-path loop
reports before continuing to the summary, the comment states the actual
contract, and a multi-path regression test pins it. Also dropped the
dead REPO_ROOT binding the same round flagged.

93/93 tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MyNR6ZrvN6M4Rr8eZbHts

Copy link
Copy Markdown
Collaborator

Pushed 750e14c addressing the final adversarial delta round at 112dd70:

# Finding Disposition
1 The guard's sys.exit escaped the reformatter's per-path except Exception loop (SystemExit is not an Exception), so a multi-path run without PyYAML died at the first path with no per-path errors and no summary Addressed --- the guard now raises NLBPinError, which the per-path loop reports before continuing to the summary; a multi-path regression test asserts each path reports and Done ( still prints.
2 The comment claimed convention parity with validate-skills.py's module-scope guard, whose clean-exit guarantee a lazy per-sentence guard cannot give Addressed --- comment rewritten to state the actual contract and why the exception form is used here.
3 Dead REPO_ROOT binding in test_slb.py Addressed --- removed.

93/93 tests. That round also verified no cache pollution from synthetic-config tests, explicit-config operation with yaml fully absent, and the perf fix (0.136s vs 9.921s pre-fix, byte-identical output).

Posted by Claude Code (AI agent) --- not written by a human.


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.

2026-08-26 23:01 UTC

@github-actions

Copy link
Copy Markdown
Contributor

Claude finished reviewView run


Correcting this: I ran scripts/check-pr-fully-clean.py, which is the correct enumeration instrument (not gh pr checks), and it returned exit 1 with real finding bullets — this PR is not currently fully clean:

❌ PR is NOT fully clean:
  - Check run 'review / claude-review' is still in status 'in_progress'
  - Check run 'build / build' is still in status 'in_progress'
  - Check run 'validate' is still in status 'in_progress'
  - No review comment has been posted evaluating HEAD SHA 750e14c5 yet

My prior message's headline ("Ready for merge, pending CI completion") was self-contradictory — a PR with in-progress required checks and no verdict-bearing review comment at its current HEAD is not ready for merge, full stop, regardless of qualifying language attached afterward. Restating accurately:

Code-level findings: unchanged from my analysis above — the new commit (750e14c5) correctly fixes the SystemExit-escaping-the-per-path-loop regression, is tested (93/93 local), and introduces no new defects. That part of the review stands.

PR status: NOT fully clean. build, validate, and review/claude-review are still in_progress on HEAD 750e14c5, and no review comment has yet evaluated this exact HEAD SHA (this review itself, once posted, will be the first). Do not merge until those checks complete green and a verdict-bearing review lands at this HEAD.

Verdict

Blocked / not clean at time of writing — CI is still in progress on HEAD 750e14c5 and no review comment yet evaluates this HEAD, per check-pr-fully-clean.py (exit 1). No new code-level defects found in the delta since the prior round (750e14c5 correctly fixes the batch-killing SystemExit regression, tested and verified). Re-check CI/check-run state once it settles before merging.

Reviewed commit: 750e14c

Stopping Point: Clean stopping point reached — this review-only task is complete. My prior message's "Ready for merge" framing was inaccurate and is superseded by this correction; no further review-only action is queued.

Reviewed commit: 750e14c

@github-actions

Copy link
Copy Markdown
Contributor

💰 Cost: $3.2861 (review) — run

@d-morrison
d-morrison merged commit 7a679a6 into main Aug 26, 2026
11 checks passed
@d-morrison
d-morrison deleted the cursor/nlb-local-ci-agree-26b4 branch August 26, 2026 23:01
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.

scripts/semantic-line-breaks.py and CI's check-new-line-breaks disagree on mid-line semicolons

4 participants