---
name: comment-lint
description: Act on the devlog/breadcrumb comment findings from scripts/lint.lg — decide per hit whether to rewrite or delete a comment, and rewrite it so it describes the code rather than the change that produced it. Use when running the comment linter, cleaning up flagged comments, reviewing a comment-cleanup diff, or writing a comment that describes why code has its current shape.
---

# comment-lint — acting on the devlog-comment report

Drives `scripts/lint.lg`, the report-only scan for comments that narrate a
change instead of describing the code. The script decides *what to look at*;
everything below is the judgement it deliberately leaves out. Rationale and the
open phrase-list questions: nooga/let-go#835.

## How to run

From the repository root:

```
lg scripts/lint.lg                  # default: pkg scripts
lg scripts/lint.lg pkg scripts cmd  # widen the scan
```

Report-only. It never edits source, and findings do not affect the exit status
(a run that finds 13 things still exits 0), so a hit is never a build gate and
never an instruction on its own.

## What a hit means

**A hit flags a comment block to re-read, not a phrase to delete.** The phrase
list matches tense markers — "used to be", "moved from", "this PR" — because
those are cheap to match and rarely appear in a description of working code.
The thing being flagged is the narration underneath, which can survive any
edit that only removes the matched words.

So the linter going quiet is not the goal. The goal is a comment that a reader
who has never seen a previous version of the file can use.

## The test

> Would this sentence still be true and useful to someone reading this file for
> the first time, who has no idea it was ever different?

If removing the comparison leaves the sentence incomplete, the comparison was
history and the sentence needs rewriting, not trimming.

## Counterfactual, yes; historical contrast, no

These two look alike and are not alike.

**Counterfactual — keep.** What goes wrong *without the code as it stands*.
This is the honest content of most regression-test and benchmark comments, and
it is frequently the right rewrite:

```go
// derefSink defeats dead-code elimination: without a package-level sink the
// compiler may elide the Deref call entirely, producing impossible sub-ns
// numbers that say nothing about the real cost.
```

A reader can check that against the code in front of them, and nothing in it
needs a previous version of the file to parse.

**Historical contrast — rewrite.** The previous design, re-tensed. The giveaway
words are `rather than`, `instead`, `not X but Y`, `alone could do without`,
`only got away without`:

```go
// It is passed to the module scaffolder rather than baked in, because the
// AOT native path shares that code.
```

`rather than baked in` only parses if you know it was once baked in. The
second clause is subtler, and it is the reason the rule above exists: the AOT
rationale is *true* — `pkg/gomod`'s package doc states it, naming #596 and the
three behaviors — but it is a fact about `pkg/gomod`, already written on
`pkg/gomod`. Restating it on a constant in `pkg/cli` is a second copy that only
the change's author would think to put there. When a kept claim checks out,
ask where it already lives before keeping it here.

**Removing the flagged phrase without removing the history is the failure mode
this skill exists to prevent.** It passes the linter and changes nothing for
the reader.

## A counterfactual is not automatically correct

Passing the test above does not make a sentence true. A rewrite that keeps the
old comment's shape keeps its claims, and the claims are the part most likely to
have gone stale:

```go
// asBytes backs the binary file/stream sinks (spit, write!). The byte-array
// case is the one that needs it: without the coercion, a byte-array handed to
// spit/write! stringifies to its #byte-array[…] repr, so bytes >127 never
// reach the sink.
```

Two defects, neither of them visible to the linter. The stringify claim is true
of `write!` — `iort.go` falls back to `vs[1].String()` — and false of `spit`,
which returns `spit expected String or byte-array` rather than writing anything.
One sentence asserts one behavior for two callers that do different things. And
where the claim does hold, `iort.go` already states it on the code itself, so
repeating it on a test is the second-copy defect from the `wasm.go` case above.

Rewrite from the function instead: it converts a `String` or a byte-kind
`TypedArray` and rejects everything else. That is checkable in one place and
stays true whatever the callers do next.

## Rewrite or delete

Do not assume every flagged comment has a salvageable core. Three outcomes,
in rough order of frequency:

1. **Trim.** The narration is a clause on an otherwise good comment. Drop the
   clause. (`the miscompile this PR fixes` → `the miscompile`.)
2. **Delete the comment.** What remains is a fact about the code's history that
   a reader of this file does not need, or a forwarding address to code that is
   already reachable by name. A pointer saying four helpers "moved to" another
   namespace tells a reader of the file they left nothing about the file they
   are in.
3. **Rewrite from the code.** The comment records something real — an
   invariant, a hazard, why an obvious simpler form is wrong — stated as
   history. Re-derive it by reading the code, not by re-tensing the sentence.

**Every causal claim you keep must name code you can point at.** A rewrite
inherits the old comment's assertions, and those were true of a tree that has
moved. Before keeping "because X shares this" or "so that Y can Z", find X and
Y in the current tree — repository-wide, including tests and build-tagged
files, not just the package in front of you. If you cannot, the claim is
history and goes; if you can, check whether the place it is already documented
is a better home than this one.

Outcome 3 is where accuracy slips. A comment written as history was true of
code that no longer exists, so its details can be stale in ways the linter
cannot see: a benchmark comment naming the fields `root` and `curr` survived a
rename to `root` and `rootBind`, and re-tensing it would have preserved the
wrong names. **Read the code before rewriting; do not paraphrase the old
comment.**

## Writing the replacement

The rewrite is not free space. Every clause you add is a claim you now own and
that someone has to maintain, so the bar for adding one is the bar for any new
comment: not "is it true" but "does a reader of *this* file need it here."

**Prefer removal to replacement.** Most flagged blocks are a good comment with a
clause welded on. Cutting the clause is finished work. Replacing the block
invites you to restate things that are already written somewhere better.

**Before adding a fact, find where it already lives.** If the claim belongs to
another file's code, that file is its home; a copy here is the `wasm.go` defect,
and it rots independently of the original.

**Stop when the sentence is checkable against the code in front of you.** Not
when it is complete, not when it explains the design — when a reader with this
file open can verify it without opening another.

**Naming a symbol buys precision and a rename hazard.** Name one when it is the
only handle a reader has, and prefer a symbol a rename would have to touch over
a file path or a line number, which nothing keeps honest.

## Two smaller rules

**Do not rank what the code does not rank.** "The byte-array case is the one
that needs it" elevates one of the three cases `TestAsBytes` covers. That
emphasis is the author's view of which case was interesting — a fact about the
change, not about the code — and it is often the re-tensed ghost of a
superlative like "the one that used to be impossible". Describe the cases and
let the reader decide which one they came for.

**Do not carry a bare issue or PR number into a block you are rewriting.** A
number that only records where a change was discussed is provenance; git blame
and the PR already hold it, and in a comment it rots silently. An identifier
that is the only name a thing has may stay: `the native adapters #438 Def'd at
init` names something with no other handle, while `(#506)` trailing a list of
three functions names nothing the sentence has not already said.

This is scoped to the block you already have open. It is not a license to sweep
every ID in the tree — that question, and the phrase list generally, is open on
nooga/let-go#835, and the split above is the current lean rather than a settled
position.

## The one hard rule

**Never drop an invariant.** Flagged comments often carry the only written
record of why something must be done in a particular order, why a simpler form
breaks, or what a test is actually pinning. Losing that to satisfy a
report-only linter is a strictly worse outcome than leaving the comment alone.
When the narration and the invariant cannot be separated, keep the comment and
say so in the PR rather than shipping a lossy rewrite.

## Two operational gotchas

- **A block reports one phrase and stops** — the first match in
  `devlog-phrases` order, not the one appearing earliest in the comment. Fixing
  it can expose a second in the same block, so re-run the linter after a sweep
  rather than trusting one pass.
- **Editing a comment in a `.lg` file that `pkg/rt/generated.manifest` lists
  changes generated artifacts.** The manifest digests its source inputs and
  `generated.sums` digests the manifest, so a comment-only edit to one needs
  `make generate`, and both files are committed. Being git-tracked is not the
  test — most `.lg` files under `pkg/` are inputs, while `scripts/lint.lg` and
  `cmd/lginterop/lginterop.lg` are not. Grep the manifest for the path. The compiled outputs are
  byte-identical — comments never reach the reader's form tree, which is also
  why the linter has to scan raw source.

The tool reports; you exercise the judgement.
