Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .claude/skills/fieldworks-code-commenting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ A C-style `/* */` block comment is not scanned -- only whole-line
rules (accuracy, WHAT-not-HOW, standalone clarity) are not mechanically
checked and still apply while authoring.

In CI a violation fails nothing. Each one lands as a warning annotation on
the diff, and the whole set as a single pull request comment that updates
in place on every push. `build.ps1 -CommentHygiene` and
`test.ps1 -CommentHygiene` are what make the same violations blocking, and
an agent passes one of them on every run.

## The standard

1. **Accuracy first, then brevity.** A wrong comment is worse than none.
Expand Down Expand Up @@ -115,22 +121,21 @@ Place above the nesting level the code spans.

**Line width is separate, and applies to every comment line**, doc comments
included: no comment line may exceed `.editorconfig`'s `max_line_length`
(98 columns today), counting a tab as `tab_width` columns. The gate reads
those two values from `.editorconfig` itself, so the limit can never drift
from the one the rest of the repo follows. Enforced as
(98 columns today), counting a tab as `tab_width` columns.
`comment-hygiene` reads those two values from `.editorconfig` itself, so the
limit can never drift from the one the rest of the repo follows. Enforced as
`comment-line-too-long`; a local run re-wraps the line for you, CI only
reports it.

**The budget rises to 600 characters in dense branching code.** A comment
introducing a region whose decision-point count reaches 10 (McCabe
complexity 11 -- the classic "high" threshold) gets the larger budget
automatically, because a reader there needs the invariants spelled out and
200 characters buys about two sentences. Nothing opts in by hand: the gate
measures the code the comment introduces, stopping at the end of the
enclosing block or 40 lines. This fires on roughly 2% of the comments
already over 200 characters, and is meant to stay that rare -- if a comment
in ordinary straight-line code will not fit, shorten it rather than looking
for a way to qualify.
200 characters buys about two sentences. `comment-hygiene` measures the code
the comment introduces, stopping at the end of the enclosing block or 40
lines. This fires on roughly 2% of the comments already over 200 characters,
and is meant to stay that rare -- if a comment in ordinary straight-line code
will not fit, shorten it rather than looking for a way to qualify.

**Exemptions from the length cap:** a C#/C/C++/IDL `///` doc comment; a
PowerShell comment-based help block (`<# ... #>`); and, in a project file or
Expand Down
4 changes: 1 addition & 3 deletions .github/instructions/build.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ FieldWorks is **Windows-first** and **x64-only**. Use the repo scripts so build
- Use `.\build.ps1` for builds and `.\test.ps1` for tests.
- Pass `-CommentHygiene` on every build and test run. It fails the run on any
comment-hygiene violation in the lines your branch adds, so you fix your own
comments before review. Humans omit it and never see the gate; CI reports
violations as warning annotations without failing. Never drop the flag to get
a run to pass.
comments before review. Never drop the flag to get a run to pass.
- Avoid ad-hoc `msbuild`/`dotnet build` invocations unless you are explicitly debugging build infrastructure.
- Do not change COM/registry behavior without an explicit plan and tests.

Expand Down
1 change: 0 additions & 1 deletion .github/instructions/testing.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ Guidance for writing and running deterministic unit and integration tests for Fi

Use `.\test.ps1` for all managed (C#) tests. Always pass `-CommentHygiene`: it
fails the run on any comment-hygiene violation in the lines your branch adds.
Humans omit it and never see the gate; CI annotates without failing.

```powershell
# Run all tests (builds first)
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ jobs:
- name: Checkout Files
uses: actions/checkout@v7
id: checkout
with:
# comment-hygiene.ps1 diffs against origin/<default-branch>; a
# shallow, single-branch checkout leaves that ref unresolvable
# and the gate fails every build with "bad revision".
fetch-depth: 0

# Real-runtime check: this repo is authored on PowerShell 7 but
# build.ps1/test.ps1 run under Windows PowerShell 5.1 below. A script
Expand All @@ -42,13 +37,17 @@ jobs:
run: |
Build/Agent/CommentHygiene.Tests.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Build/Agent/Test-BuildCommentHygieneComment.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Comment hygiene fixture tests (Windows PowerShell 5.1)
id: comment-hygiene-tests-winps
shell: powershell
run: |
Build\Agent\CommentHygiene.Tests.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Build\Agent\Test-BuildCommentHygieneComment.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Build with tests
id: build
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/CommentHygiene.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Comment hygiene check
on:
pull_request:

permissions:
contents: read
# pull-requests: write lets the sticky-comment steps post and clear the
# comment-hygiene comment on the PR.
pull-requests: write

# Avoid unnecessary runs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
# Separate from the build so the report lands on the pull request in about a
# minute instead of waiting on Debug plus tests, and still arrives when the
# build fails or is superseded.
comment_hygiene:
name: Report comment hygiene
runs-on: windows-2022
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# comment-hygiene.ps1 diffs against origin/<base branch>; a shallow,
# single-branch checkout leaves that ref unresolvable.
fetch-depth: 0

# Windows PowerShell 5.1 on windows-2022: the engine and platform the
# comment-hygiene scripts are exercised under everywhere else in CI.
- name: Scan the lines this pull request adds
shell: powershell
continue-on-error: true
run: |
.\Build\Agent\comment-hygiene.ps1 -Advisory -ReportPath .\Output\CommentHygiene\report.json

- name: Compose the pull request comment
id: comment
shell: powershell
continue-on-error: true
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
.\Build\Agent\Build-CommentHygieneComment.ps1 -ReportPath .\Output\CommentHygiene\report.json

# Advisory to the end: a comment this workflow cannot post -- a pull request
# from a fork gets a read-only token -- must not read as a failed check.
- name: Post the comment-hygiene summary
if: ${{ steps.comment.outputs.has_violations == 'true' }}
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: fieldworks-comment-hygiene
path: ${{ steps.comment.outputs.comment_path }}
skip_unchanged: true

# only_update: a clean pull request has nothing to say, so this replaces an
# earlier summary where one exists and posts nothing where none does.
- name: Clear the comment-hygiene summary
if: ${{ steps.comment.outputs.has_violations == 'false' }}
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: fieldworks-comment-hygiene
only_update: true
path: ${{ steps.comment.outputs.comment_path }}
skip_unchanged: true
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ Minimal, high-signal guidance for coding agents in this repository.
- Test with `.\test.ps1 -CommentHygiene`.
- `-CommentHygiene` is required of agents and not of humans: it fails the run on
any comment-hygiene violation in the lines your branch adds, so you fix your
own comments before they reach review. A developer build omits it and never
runs the gate; CI reports violations as warning annotations without failing.
Do not drop the flag to get a build through.
own comments before they reach review. Do not drop the flag to get a build
through.
- Do not bypass repository scripts for normal build/test work.
- Commit messages must pass `gitlint` (CI: `.github/workflows/CommitMessage.yml`):
title <=72 characters, body lines <=80 characters, blank line between
Expand Down
213 changes: 213 additions & 0 deletions Build/Agent/Build-CommentHygieneComment.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<#
.SYNOPSIS
Renders the comment-hygiene pull request comment from a scan report.

.DESCRIPTION
Turns the report comment-hygiene.ps1 writes under -ReportPath into a markdown
body for a sticky pull request comment, and publishes comment_path and
has_violations as GitHub step outputs so the workflow can pick between posting
and clearing. Violations are listed as a table of file links, categories, and
comment text, capped at -MaxListed rows.

.PARAMETER ReportPath
The JSON report comment-hygiene.ps1 writes. A missing file is an error, not an
empty report: a scan always writes one.

.PARAMETER CommentPath
Where to write the markdown body. Defaults beside the report.

.PARAMETER MaxListed
How many violations the table carries before it summarizes the rest as a
count.

.PARAMETER Sha
Commit the file links resolve against. Pass a pull request's head commit;
links fall back to plain text when this or Repository is unset.

.EXAMPLE
Build/Agent/Build-CommentHygieneComment.ps1 -ReportPath Output/CommentHygiene/report.json
#>
[CmdletBinding()]
param(
[string]$CommentPath,
[string]$GitHubOutputPath = $env:GITHUB_OUTPUT,
[int]$MaxListed = 25,
[string]$ReportPath,
[string]$Repository = $env:GITHUB_REPOSITORY,
[string]$RunId = $env:GITHUB_RUN_ID,
[string]$ServerUrl = $env:GITHUB_SERVER_URL,
[string]$Sha = $env:HEAD_SHA
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if ([string]::IsNullOrWhiteSpace($ReportPath)) {
$ReportPath = Join-Path (Join-Path $repoRoot 'Output\CommentHygiene') 'comment-hygiene-report.json'
}
if ([string]::IsNullOrWhiteSpace($CommentPath)) {
$CommentPath = Join-Path (Split-Path -Path $ReportPath -Parent) 'comment-hygiene-comment.md'
}
if ([string]::IsNullOrWhiteSpace($ServerUrl)) {
$ServerUrl = 'https://github.com'
}
if ([string]::IsNullOrWhiteSpace($Sha)) {
$Sha = $env:GITHUB_SHA
}

# PowerShell's current location and .NET's working directory can differ; this script
# reads through the first and writes through the second, so anchor both paths once.
if (-not [System.IO.Path]::IsPathRooted($ReportPath)) {
$ReportPath = Join-Path (Get-Location).Path $ReportPath
}
if (-not [System.IO.Path]::IsPathRooted($CommentPath)) {
$CommentPath = Join-Path (Get-Location).Path $CommentPath
}

$SkillPath = '.claude/skills/fieldworks-code-commenting/SKILL.md'

function Write-GitHubOutputValue {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Value
)

if ([string]::IsNullOrWhiteSpace($GitHubOutputPath)) {
return
}

# UTF8Encoding($false): a byte-order mark part way through the output file breaks
# the runner's parse of every value after it.
$encoding = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::AppendAllText($GitHubOutputPath, "$Name=$Value$([System.Environment]::NewLine)", $encoding)
}

function Format-TableCell {
<#
.SYNOPSIS
Renders comment text as a single-line markdown code span, truncated to
-MaxLength characters.
#>
param(
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$Value,
[int]$MaxLength = 90
)

$collapsed = ($Value -replace '\s+', ' ').Trim()
if ([string]::IsNullOrEmpty($collapsed)) {
return ''
}
if ($collapsed.Length -gt $MaxLength) {
$collapsed = $collapsed.Substring(0, $MaxLength).TrimEnd() + '...'
}

# GitHub splits a table row on every unescaped pipe, inside a code span as much
# as outside one. A fence has to outnumber any backtick it wraps.
$escaped = $collapsed -replace '\|', '\|'
$fence = if ($escaped.Contains('`')) { '``' } else { '`' }
if ($escaped.StartsWith('`') -or $escaped.EndsWith('`')) {
$escaped = " $escaped "
}

return "$fence$escaped$fence"
}

function Format-FileLink {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[int]$Line
)

$label = "${Path}:$Line"
if ([string]::IsNullOrWhiteSpace($Repository) -or [string]::IsNullOrWhiteSpace($Sha)) {
return "``$label``"
}

return "[$label]($ServerUrl/$Repository/blob/$Sha/$Path#L$Line)"
}

function Get-RunReference {
if ([string]::IsNullOrWhiteSpace($Repository) -or [string]::IsNullOrWhiteSpace($RunId)) {
return 'this check''s log'
}

return "[this check's log]($ServerUrl/$Repository/actions/runs/$RunId)"
}

if (-not (Test-Path -LiteralPath $ReportPath)) {
throw "Comment-hygiene report not found: $ReportPath"
}

$report = Get-Content -LiteralPath $ReportPath -Raw | ConvertFrom-Json
foreach ($field in @('base', 'violationCount', 'violations')) {
if ($null -eq $report.PSObject.Properties[$field]) {
throw "Comment-hygiene report is missing the '$field' field: $ReportPath"
}
}

$violationCount = [int]$report.violationCount
$baseLabel = [string]$report.base
$hasViolations = $violationCount -gt 0

if ($hasViolations) {
$listed = @(@($report.violations) | Select-Object -First $MaxListed)
$commentLines = @(
'### Comment hygiene (advisory)'
''
"$violationCount comment-style violation(s) in the lines this branch adds since ``$baseLabel``."
'Advisory only -- no check fails on these, and the same violations appear as inline warnings on the Files changed tab.'
''
'| File | Category | Comment |'
'| --- | --- | --- |'
)

foreach ($violation in $listed) {
$commentLines += ('| {0} | `{1}` | {2} |' -f
(Format-FileLink -Path ([string]$violation.file) -Line ([int]$violation.line)),
([string]$violation.category),
(Format-TableCell -Value ([string]$violation.text)))
}

$commentLines += ''
if ($violationCount -gt $listed.Count) {
$commentLines += "$($violationCount - $listed.Count) more not listed here -- see $(Get-RunReference)."
$commentLines += ''
}

$commentLines += "Fix them per ``$SkillPath``."
$commentLines += 'Running `.\build.ps1 -CommentHygiene` (or `.\test.ps1 -CommentHygiene`) enforces them locally, and'
$commentLines += 're-wraps over-wide lines and repairs non-ASCII punctuation as it goes.'
}
else {
$commentLines = @(
'### Comment hygiene (advisory)'
''
"No comment-style violations in the lines this branch adds since ``$baseLabel``."
)
}

$commentDirectory = Split-Path -Path $CommentPath -Parent
if (-not [string]::IsNullOrWhiteSpace($commentDirectory) -and -not (Test-Path -LiteralPath $commentDirectory)) {
New-Item -ItemType Directory -Path $commentDirectory -Force | Out-Null
}

$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($CommentPath, ($commentLines -join [System.Environment]::NewLine), $utf8NoBom)

$resolvedCommentPath = [System.IO.Path]::GetFullPath($CommentPath)
Write-GitHubOutputValue -Name 'comment_path' -Value $resolvedCommentPath
Write-GitHubOutputValue -Name 'has_violations' -Value ($hasViolations.ToString().ToLowerInvariant())
Write-GitHubOutputValue -Name 'violation_count' -Value ([string]$violationCount)

Write-Output ([pscustomobject]@{
CommentPath = $resolvedCommentPath
HasViolations = $hasViolations
ViolationCount = $violationCount
})
Loading
Loading