Skip to content

Commit 8a9c8fc

Browse files
feat: cap comment width at .editorconfig, extend budget in complex code
Adds a comment-line-too-long rule that reads max_line_length and tab_width from .editorconfig, so the comment limit is the one the rest of the repo already follows. It counts display columns rather than characters, since this repo indents with tabs, and it applies to doc comments too: a doc comment is exempt from the content budget because of what it says, not because it may run off the screen. A local run re-wraps the line, CI only reports it. The 200-character content budget rises to 600 where the code a comment introduces scores at least 10 decision points. Measure-CodeComplexity counts them, stopping at the end of the enclosing block or after 40 lines. Measured across the whole repo, that extends 2 percent of the comments already over 200, which is the intended rarity. The gate then re-wrapped this branch's own over-long comments across 210 files. Six needed shortening by hand, and XML comment indentation is normalized to tabs so the new lines pass git's indent-with-non-tab check. Also seeds two script-scoped variables before they are read: Set-StrictMode throws on an unset variable, which powershell-compat.ps1 hit on every run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 07cac06 commit 8a9c8fc

210 files changed

Lines changed: 2401 additions & 960 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/fieldworks-code-commenting/SKILL.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,25 @@ sentence that would confuse a reader most if missing, even if that loses
113113
nuance. Mechanically enforced (`comment-too-long`) for `//` and `#` alike.
114114
Place above the nesting level the code spans.
115115

116+
**Line width is separate, and applies to every comment line**, doc comments
117+
included: no comment line may exceed `.editorconfig`'s `max_line_length`
118+
(98 columns today), counting a tab as `tab_width` columns. The gate reads
119+
those two values from `.editorconfig` itself, so the limit can never drift
120+
from the one the rest of the repo follows. Enforced as
121+
`comment-line-too-long`; a local run re-wraps the line for you, CI only
122+
reports it.
123+
124+
**The budget rises to 600 characters in dense branching code.** A comment
125+
introducing a region whose decision-point count reaches 10 (McCabe
126+
complexity 11 -- the classic "high" threshold) gets the larger budget
127+
automatically, because a reader there needs the invariants spelled out and
128+
200 characters buys about two sentences. Nothing opts in by hand: the gate
129+
measures the code the comment introduces, stopping at the end of the
130+
enclosing block or 40 lines. This fires on roughly 2% of the comments
131+
already over 200 characters, and is meant to stay that rare -- if a comment
132+
in ordinary straight-line code will not fit, shorten it rather than looking
133+
for a way to qualify.
134+
116135
**Exemptions from the length cap:** a C#/C/C++/IDL `///` doc comment; a
117136
PowerShell comment-based help block (`<# ... #>`); and, in a project file or
118137
Avalonia view, the file's FIRST `<!-- -->` block, wherever it falls (before

Build/Agent/CommentHygiene.Tests.ps1

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,21 @@ function Assert-Unrepairable {
105105
Assert-Category 'phase-framing' '// Phase 3 test (b): picking a style applies it to the selection' 'process-framing'
106106
Assert-Clean 'phase-clean' '// Applies the selected style to the current selection'
107107

108-
# process-framing must not fire on "stage"/"commit" here: this codebase has a real two-step stage-then-commit UI pattern, distinct from migration-phase framing.
108+
# process-framing must not fire on "stage"/"commit" here: this codebase has a real two-step
109+
# stage-then-commit UI pattern, distinct from migration-phase framing.
109110
Assert-Clean 'stage-domain-term' '// STAGE 1 -- the pick already populated the auxiliary picker.'
110111
Assert-Clean 'commit-domain-term' '// Capture everything staged since the last boundary -- that is what this commit "writes".'
111112

112113
# doc-pointer -- skill worked example
113114
Assert-Category 'doc-pointer-md' '// winforms-free-lexeme-editor.md D1: a plugin-claimed custom slice renders its plugin''s own control' 'doc-pointer'
114115
Assert-Clean 'doc-pointer-clean' '// LT-22351: a plugin-claimed custom slice renders its plugin''s own control'
115116

116-
# doc-pointer finding-code must stay case-sensitive: "m3" is a real LCM field name (IMoStemMsa), not a design-doc finding code, despite the (?i) flag earlier in the pattern.
117+
# doc-pointer finding-code must stay case-sensitive: "m3" is a real LCM field name (IMoStemMsa),
118+
# not a design-doc finding code, despite the (?i) flag earlier in the pattern.
117119
Assert-Clean 'doc-pointer-lowercase-field' '// Seed text matches the canonical field label (the m3 InflectionClass field label).'
118120

119-
# doc-pointer finding-code must not fire on function keys or generic type-parameter names, which share the letter-plus-digit shape but are ordinary code vocabulary, not design-doc codes.
121+
# doc-pointer finding-code must not fire on function keys or generic type-parameter names, which
122+
# share the letter-plus-digit shape but are ordinary code vocabulary, not design-doc codes.
120123
Assert-Clean 'doc-pointer-function-key' '// Whether it came from a legacy view, F5/RefreshAllViews-driven, or something else.'
121124
Assert-Clean 'doc-pointer-generic-param' '// Factored to a Func<T1,T2,TResult> seam so the caller can inject either path.'
122125

@@ -157,7 +160,8 @@ Assert-Repair 'repair-noop-cjk' $cjkLine $cjkLine
157160
# Repair-CommentLine -- not a whole-line comment at all
158161
Assert-Unrepairable 'repair-not-a-comment' ("int x = 1; // trailing {0} comment" -f (Get-Codepoint 0x2014))
159162

160-
# PowerShell (#) gets the same categories as C# (//) -- the gap that let this tooling's own comments ship unscanned.
163+
# PowerShell (#) gets the same categories as C# (//) -- the gap that let this tooling's own
164+
# comments ship unscanned.
161165
Assert-Category 'ps-phase-framing' '# Phase 3 test: picking a style applies it to the selection' 'process-framing' '.ps1'
162166
Assert-Category 'ps-non-ascii-punctuation' ("# Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) 'non-ascii-punctuation' '.ps1'
163167
Assert-Repair 'repair-ps-em-dash' ("# Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) '# Uses an em dash -- inline.'
@@ -197,13 +201,15 @@ Assert-CleanLines 'help-block-long-ps' @(
197201
'#>'
198202
) '.ps1'
199203

200-
# CLike (C/C++/IDL) shares C#'s // and /// syntax and categories -- one worked example per extension.
204+
# CLike (C/C++/IDL) shares C#'s // and /// syntax and categories -- one worked example per
205+
# extension.
201206
Assert-Category 'cpp-phase-framing' '// Phase 3 test: picking a style applies it to the selection' 'process-framing' '.cpp'
202207
Assert-Category 'h-non-ascii-punctuation' ("// Uses an em dash {0} inline." -f (Get-Codepoint 0x2014)) 'non-ascii-punctuation' '.h'
203208
Assert-Category 'idl-absence-narration' '// An ORC run no longer forces the whole value read-only.' 'absence-narration' '.idl'
204209
Assert-Clean 'cpp-clean' '// Applies the selected style to the current selection' '.cpp'
205210

206-
# Xml uses <!-- -->; banned categories fire regardless of exempt/impl kind, even on the first comment.
211+
# Xml uses <!-- -->; banned categories fire regardless of exempt/impl kind, even on the first
212+
# comment.
207213
Assert-Category 'csproj-single-line' '<!-- This property no longer affects the build output. -->' 'absence-narration' '.csproj'
208214
Assert-Clean 'csproj-single-line-clean' '<!-- This property controls the build output. -->' '.csproj'
209215
Assert-CategoryLines 'axaml-multi-line' @(
@@ -212,10 +218,15 @@ Assert-CategoryLines 'axaml-multi-line' @(
212218
'-->'
213219
) 'absence-narration' '.axaml'
214220

215-
# Xml's FIRST <!-- --> block in a file is exempt from the length budget (its /// equivalent);
216-
# every later block is an ordinary budgeted implementation comment.
221+
# Xml's FIRST block is exempt from the budget; later blocks are not. Over 200
222+
# chars total, but each physical line fits the width limit: the rules are
223+
# independent.
217224
Assert-CleanLines 'xml-first-comment-exempt-long' @(
218-
'<!-- This is the file-level summary comment, and it is allowed to run long-form the same way a C# /// doc comment or a PowerShell comment-based help block can, because Xml has no other syntax to mark one. -->',
225+
'<!--',
226+
' This is the file-level summary comment, and it is allowed to run long-form the',
227+
' same way a C# /// doc comment or a PowerShell comment-based help block can,',
228+
' because Xml has no other syntax to mark a file or type level summary.',
229+
'-->',
219230
'<Project Sdk="Microsoft.NET.Sdk">',
220231
'</Project>'
221232
) '.csproj'
@@ -237,6 +248,104 @@ Assert-CleanLines 'targets-clean-multiline' @(
237248
Assert-Category 'xml-double-hyphen' '<!-- Uses a real dash -- here, which is illegal inside an XML comment. -->' 'xml-illegal-double-hyphen' '.csproj'
238249
Assert-Clean 'xml-single-hyphen-clean' '<!-- Uses a real dash - here, which is legal inside an XML comment. -->' '.csproj'
239250

251+
252+
# ---- per-line width, taken from .editorconfig's max_line_length ----
253+
254+
$cfg = Get-CommentHygieneEditorConfig -RepoRoot (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
255+
if ($cfg.MaxLineLength -ne 98 -or $cfg.TabWidth -ne 4) {
256+
[void]$failures.Add("FAIL [editorconfig-read]: expected 98/4, got $($cfg.MaxLineLength)/$($cfg.TabWidth)")
257+
}
258+
259+
$widthPad = '/' * 100
260+
Assert-Category 'comment-line-too-long' "// $widthPad" 'comment-line-too-long'
261+
Assert-Clean 'comment-line-at-limit' ('// ' + ('x' * 94))
262+
263+
# A tab counts as four display columns, not one: 24 tabs plus a short comment is
264+
# already past the limit even though the string is well under 98 characters.
265+
$tabbed = ("`t" * 24) + '// short'
266+
Assert-Category 'comment-line-tabs-count-as-columns' $tabbed 'comment-line-too-long'
267+
268+
if ((Get-CommentDisplayWidth -Line "`tab" -TabWidth 4) -ne 6) {
269+
[void]$failures.Add('FAIL [display-width-tab-stop]: expected a leading tab to advance to column 4')
270+
}
271+
272+
# Doc comments are exempt from the content budget but not from the width rule.
273+
Assert-Category 'doc-comment-too-wide' "/// $widthPad" 'comment-line-too-long'
274+
275+
$wrapSource = "`t// " + ('the quick brown fox jumps over the lazy dog and keeps running ' * 3)
276+
$wrapped = Format-CommentLineWrap -Line $wrapSource -Language 'CLike' -MaxWidth 98 -TabWidth 4
277+
if ($null -eq $wrapped -or $wrapped.Count -lt 2) {
278+
[void]$failures.Add('FAIL [wrap-splits]: expected an over-long comment to wrap onto multiple lines')
279+
}
280+
else {
281+
foreach ($line in $wrapped) {
282+
if (-not $line.TrimStart().StartsWith('//')) {
283+
[void]$failures.Add("FAIL [wrap-prefix]: continuation line lost its comment prefix: $line")
284+
}
285+
}
286+
if ((Get-CommentDisplayWidth -Line $wrapped[0] -TabWidth 4) -gt 98) {
287+
[void]$failures.Add('FAIL [wrap-width]: wrapped line still exceeds the limit')
288+
}
289+
}
290+
291+
if ($null -ne (Format-CommentLineWrap -Line '// short' -Language 'CLike' -MaxWidth 98 -TabWidth 4)) {
292+
[void]$failures.Add('FAIL [wrap-noop]: a line that already fits must not be rewrapped')
293+
}
294+
295+
# A single-line Xml comment expands into the delimiters-on-their-own-lines form.
296+
$xmlSource = '<!-- ' + ('the quick brown fox jumps over the lazy dog and keeps running ' * 3) + '-->'
297+
$xmlWrapped = Format-CommentLineWrap -Line $xmlSource -Language 'Xml' -MaxWidth 98 -TabWidth 4
298+
if ($null -eq $xmlWrapped -or $xmlWrapped.Count -lt 3) {
299+
[void]$failures.Add('FAIL [wrap-xml-expands]: expected an over-long Xml comment to expand to multiple lines')
300+
}
301+
else {
302+
if ($xmlWrapped[0].Trim() -ne '<!--' -or $xmlWrapped[-1].Trim() -ne '-->') {
303+
[void]$failures.Add('FAIL [wrap-xml-delimiters]: expected the delimiters to move onto their own lines')
304+
}
305+
foreach ($line in $xmlWrapped) {
306+
if ((Get-CommentDisplayWidth -Line $line -TabWidth 4) -gt 98) {
307+
[void]$failures.Add("FAIL [wrap-xml-width]: wrapped Xml line still exceeds the limit: $line")
308+
}
309+
}
310+
}
311+
312+
# ---- complexity raises the content budget, but only in dense branching code ----
313+
314+
# Wrapped inside the width limit so these fixtures exercise the content budget
315+
# alone; an over-wide single line would trip comment-line-too-long instead.
316+
$longComment = @(
317+
'// The invariant this restores is subtle and needs spelling out for the',
318+
'// next reader, because the branching below depends on it holding at each',
319+
'// step and nothing in the code itself says so. Without this note the next',
320+
'// person reads the guard as redundant and deletes it.'
321+
)
322+
323+
function New-BranchyFixture {
324+
param([string[]] $Comment)
325+
$out = New-Object System.Collections.ArrayList
326+
foreach ($line in $Comment) { [void]$out.Add($line) }
327+
[void]$out.Add('void M(int n) {')
328+
for ($k = 0; $k -lt 12; $k++) { [void]$out.Add(" if (n == $k && n > 0) { DoWork($k); }") }
329+
[void]$out.Add('}')
330+
return ,$out.ToArray()
331+
}
332+
333+
$simpleCode = @($longComment) + @('void M() {', ' DoWork();', '}')
334+
Assert-CategoryLines 'long-comment-simple-code-flagged' $simpleCode 'comment-too-long'
335+
Assert-CleanLines 'long-comment-complex-code-allowed' (New-BranchyFixture -Comment $longComment)
336+
337+
# The extended budget is a higher ceiling, not the removal of one.
338+
$hugeComment = $longComment + $longComment + $longComment
339+
Assert-CategoryLines 'extended-budget-still-capped' (New-BranchyFixture -Comment $hugeComment) 'comment-too-long'
340+
341+
$cxSimple = Measure-CodeComplexity -Lines @('void M() {', ' DoWork();', '}') -StartIndex 0 -Language 'CLike'
342+
if ($cxSimple -ge 10) {
343+
[void]$failures.Add("FAIL [complexity-simple]: straight-line code scored $cxSimple")
344+
}
345+
if ((Measure-CodeComplexity -Lines @('<Project>') -StartIndex 0 -Language 'Xml') -ne 0) {
346+
[void]$failures.Add('FAIL [complexity-xml]: Xml has no control flow and must score zero')
347+
}
348+
240349
Remove-Item -LiteralPath $tempDir -Recurse -Force
241350

242351
if ($failures.Count -gt 0) {
@@ -248,4 +357,4 @@ if ($failures.Count -gt 0) {
248357
}
249358

250359
Write-Host 'All CommentHygiene tests passed.' -ForegroundColor Green
251-
exit 0
360+
exit 0

0 commit comments

Comments
 (0)