Skip to content

Commit e4bd329

Browse files
reid-spencerclaude
andcommitted
Record the rule-id mechanism, the println trap, and multi-line do
Three durable facts from today that a fresh session would otherwise re-derive or get wrong. The rule-id entry leads with the thing most likely to be duplicated: RuleId GENERALIZES Messages.DeprecationCode rather than sitting beside it, and an early draft's REF001 scheme was dropped precisely because it would have been a second competing set of ids. It also records why the enum exists at all -- the hand-maintained list that twice omitted a whole deprecation family from reports calling themselves exhaustive. The println entry is the one worth reading before writing another command test. A bare println is Console.println, whose stream is a thread-local fixed at class load, so a command printing from inside a Future writes to the real stdout even under System.setOut. Production is fine; the TEST reads an empty string and that looks exactly like the "printed nothing" defect the whole test family exists to catch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 21e6525 commit e4bd329

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,94 @@ to the right group rather than appending to a list.
16481648
Opaque type `Contents[?]` erases to `ArrayBuffer` at runtime,
16491649
so `case (_: Contents[?], …)` matches correctly.
16501650

1651+
### Diagnostic rule ids — every message names the RULE that produced it
1652+
1653+
**`RuleId`** (`language/.../RuleId.scala`) is a kebab-case, subject-prefixed enum: 303 rules
1654+
covering all 307 diagnostic sites. `Message.ruleId: Option[RuleId]`, and `ruleId` is a
1655+
**REQUIRED** parameter on the eight `Accumulator.add*` helpers — a new diagnostic does not
1656+
compile until it names its rule. The six calls in `MessagesTest` pass `None` explicitly.
1657+
1658+
**It GENERALIZES `Messages.DeprecationCode`; it does not sit beside it.** That object was
1659+
already a threaded kebab-case id registry for deprecations, consumed at `RiddlLib.scala:970`
1660+
to build `SourceEdit`s. Its 12 codes are reproduced EXACTLY — including `prompt-statement`,
1661+
whose rule was renamed `DoStatement` while its code deliberately was not, because renaming a
1662+
rule is a source change and renaming its code is an API break. **Do not introduce a second
1663+
scheme** (an early draft proposed `REF001`-style ids; it was dropped for exactly this).
1664+
1665+
**An id names a RULE, not a site.** Four rules are emitted from more than one place on
1666+
purpose — `ref-wrong-kind` from BOTH `ReferenceMap.definitionOf` and
1667+
`ResolutionPass.wrongType`, which is apt given those two paths once disagreed about whether
1668+
to check the kind at all.
1669+
1670+
**Non-reuse is enforced by CODE, in three parts** (all canary-tested by breaking them):
1671+
`values` is generated so codes are checked unique; `RuleId.retired` names withdrawn codes and
1672+
no live code may appear there; and a committed **append-only ledger**
1673+
(`language/src/test/resources/rule-ids.txt`) catches what the in-memory checks cannot see — a
1674+
rule DELETED without retiring its code, which is the one at risk of being reused later.
1675+
**`RuleId.grandfathered` is CLOSED**: the 12 legacy codes predate the subject scheme and are
1676+
exempt from it. A new rule that fits no subject needs a SUBJECT added, never an exemption.
1677+
1678+
**Why the enum at all**: `DeprecationCode.all` was a hand-maintained `Seq` beside the
1679+
definitions, and TWICE a code was defined but never added to it — `entity-option-to-intention`
1680+
for months — so "exhaustive" migration reports silently omitted a whole family. `all` and the
1681+
mechanical-replacement map are DERIVED now; there is no second list to forget.
1682+
1683+
**The id renders in the LOGGER, not in `Message.format`.** The logger already supplies the
1684+
kind prefix, so output reads `[error] [use-unused-definition] file(...)`, rustc's shape.
1685+
`format` is what `CheckMessagesTest` compares its 13 goldens against, so putting it there
1686+
churned every one of them for a fact those files do not exist to pin. **`--no-msg-ids`**
1687+
(`CommonOptions.showMessageIds`, default TRUE) restores the previous output exactly.
1688+
1689+
**`validate --json`** emits one object per diagnostic on stdout (rule, severity, message,
1690+
file, line, col, and context/suggestion when present); `[]` when clean, never empty output.
1691+
**`validate --fix` / `--fix-rule <id>`** applies the codemod a rule carries
1692+
(`RuleId.mechanicalFix`), through the SAME gate as `find -replace`
1693+
`FindEditor.applyVerified`, lifted so there is one copy rather than two. Only PURE SPAN
1694+
replacements qualify: `type-first-aggregate` is a reordering and `shape-keyword` inserts
1695+
outside the reported span, so both are excluded rather than approximated. See BACKLOG [1.16]
1696+
for `quoted-constant-literal`, which is genuinely mechanical but needs a COMPUTED replacement
1697+
an `Option[String]` cannot express.
1698+
1699+
**`FindEditor.fileOfSource`, never `Path.of(loc.source.origin)`.** `origin` is the SHORT name
1700+
error messages render, so treating it as a path works only when the cwd happens to be the
1701+
model's own directory — how `find -replace` originally shipped, and a bug `validate --fix`
1702+
nearly reintroduced the same day.
1703+
1704+
### A bare `println` is invisible to a test that redirects stdout
1705+
1706+
**`println` is `Console.println`, and `Console.out` is a THREAD-LOCAL initialised at class
1707+
load.** `System.setOut` therefore does not redirect it, and code printing from inside a
1708+
`Future` — on an executor thread — writes to the real stdout regardless. In production the two
1709+
name the same object and nothing is wrong with the output; **under capture the test reads an
1710+
empty string, which presents as exactly the "command printed nothing" defect** the whole
1711+
`ValidateSummaryTest`/`ProductGoesToStdoutTest` family exists to detect. A false positive from
1712+
the instrument, not the code.
1713+
1714+
**Emit a command's product with `System.out.println`.** `ValidateCommand.emitJson` and
1715+
`DumpCommand.emit` both do. `StdStreamCapture` also wraps `Console.withOut`, which closes the
1716+
same-thread half but CANNOT help across threads — the `System.out` form is what does.
1717+
1718+
### Multi-line `do` and `prompt` (rc.25+)
1719+
1720+
`do { "a" "b" "c" }` and `prompt({ "a" "b" })`, with the bare single-string form unchanged.
1721+
The braced shape is **`doc_block`'s**, already RIDDL's spelling for prose, so no new syntax
1722+
idiom was invented. **The bare form takes EXACTLY ONE string**: `do "a" "b"` by juxtaposition
1723+
parses unambiguously (nothing else begins with a quote) but leaves nothing except the next
1724+
keyword to mark where the statement ends.
1725+
1726+
`DoStatement.what` and `PromptValue.prompt` are `Seq[LiteralString]`; **`.text`** derives the
1727+
`\n`-separated prose riddlg reads. Derived, not stored, so there is no second field to
1728+
disagree — and a single-line `do` is a Seq of one rather than a special case.
1729+
1730+
**Additive at every layer, and that is load-bearing.** A one-line `do` prettifies
1731+
byte-identically to before and serializes as a bare JSON string rather than an array, so none
1732+
of the corpus's 190 models move for a feature they do not use. Several lines get ONE PER LINE
1733+
inside braces — the layout every other block uses; squashing them onto one line would be the
1734+
narrow second copy of a block dispatch `InvariantBlock` was already caught being.
1735+
**BAST `FORMAT_REVISION` 23**: both now write a SEQUENCE where they wrote a bare string, so a
1736+
revision-22 file's string is read as a COUNT and everything after it derails. The JSON reader
1737+
accepts a string OR an array, so nothing already written stops loading.
1738+
16511739
### Total Dispatch — no silent fall-through
16521740

16531741
**Reid's standing rule (2026-08-09): "There must be no non-sealed matches — it

0 commit comments

Comments
 (0)