Repository navigation
Conversation
A constant is a decision the program has made -- a limit, a retry count, a path, a key, a magic number somebody named. Scattered across the tree those decisions cannot be read as a set, nobody can say which are still true, and the same one gets made twice under two names. const-files names where they live and every declaration outside is an error. It is theme-files for constants. Deliberately not a parser. A rule that has to tell one expression from another needs a tree, which is why test-quality fetches a grammar. This one asks whether a line *declares* a screaming-snake name, and declaration syntax answers that alone -- so it covers all eighteen languages straitjacket calls structured code rather than the nine with a pack, and never reaches the network. It is opt-in only because designating the files is a decision no default can make, and enabling it with no const-files is refused rather than obeyed: every constant would be a finding with nowhere to move it. Three decisions that keep it a rule rather than a nuisance. Declarations, not uses -- referencing a constant is the point of having one, and flagging that would make the rule unsatisfiable. At least two words joined by an underscore, because PI, OK, HTTP, a Go export and a C header guard are all single all-caps words and flagging them would bury the constants among them. And a bare NAME = value counts as a declaration only where the language spells it that way -- in Python, Ruby and Shell only at the left margin, which is what keeps every enum member and every shell local from being a finding. To read code rather than text, rules/comments.rs now yields the comments and a code-only view from one traversal. Masking preserves byte offsets, so a column in the masked line is the column in the real one, and a declaration commented out or quoted inside a string is not one. One lexer answers where the code stops and the prose starts; answering it in two places is how the two answers come to disagree. Run against this repository it reports twenty constants across Rust, TypeScript and shell -- every one a real declaration, with imports, uses and indented shell locals correctly left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WrSzGnURZoupdEfVdwk9pg
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
straitjacket | 6da1335 | Commit Preview URL Branch Preview URL |
Aug 30 2026, 08:42 PM |
The first cut matched declaration syntax with a per-language table of keyword patterns. That table is a parser written badly: it cannot tell `MAX_SIZE` in `const MAX_SIZE = 3` from `MAX_SIZE` in `n > MAX_SIZE` without knowing each language's declaration grammar, and every language added means another pattern to get subtly wrong. The question is a tree question, so it moves to beamte, which owns what a construct *is*. `const-declaration` asks the node vocabulary instead: a `_binding` that is neither an import nor a parameter, outside any callable, whose bound name is SCREAMING_SNAKE. That needs no language table at all -- only which pack serves which grammar. What is left here is what beamte refuses to do: fetch a grammar, parse, pick a severity, and name the files constants belong in. The last of those is the policy half, and it stays policy: beamte reports every declaration and `const-files` decides which are licensed. Costs the change accepts: ten languages instead of eighteen, and a grammar download on first scan, which is why the rule is off by default twice over. Gains: no false positive on a use, and shell reassignment in `publish.sh` now reads as the declaration it is. `comments.rs` reverts to main -- the masking it grew existed only to keep the regex off commented-out code, and a parse does that for free. `pack::cached` moves out of `test_quality` so both hosts share one cache rather than each keeping a private one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WrSzGnURZoupdEfVdwk9pg
A `[patch.crates-io]` entry naming a branch stops resolving the moment that branch is merged and deleted, and the failure lands on whoever pushes next rather than on whoever merged. That is how #63's CI broke: beamte's branch went away under a PR that had not changed, and cargo could not resolve the dependency before a single check ran. A merged commit stays reachable, so the rev survives the merge. The table is temporary either way -- `beamte = "0.3"` above it is already written for the registry, and this whole block goes when 0.3 publishes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WrSzGnURZoupdEfVdwk9pg
|
A constant is a decision the program has made — a limit, a retry count, a path, a key, a magic number somebody named. Scattered across the tree those decisions cannot be read as a set, nobody can say which are still true, and the same one gets made twice under two names.
stray-constreports aSCREAMING_SNAKE_CASEdeclaration anywhere but the files you designate;const-filesistheme-filesfor constants.Or
--stray-const. Enabling it with noconst-filesis refused rather than obeyed: every constant would be a finding with nowhere to move it, which is a configuration nobody means.The analysis is beamte's
The first cut of this rule matched declaration syntax with a per-language table of regexes, here in straitjacket. That was wrong, and the second commit replaces it.
That table is a parser written badly. The entire content of the rule is the difference between declaring a name and using one, and text cannot tell those apart without knowing each language's declaration grammar — so every language added meant another pattern to get subtly wrong, and each one had to re-answer where a string literal ends and where a comment starts.
So the analysis moved to beamte, which owns what a construct is: PowderworksCode/beamte#23 adds
const-declaration, which asks the node vocabulary instead — a_bindingthat is neither a_directivenor a_parameter, outside any_callable. One form, every grammar, no table to drift.What remains in this PR is everything beamte refuses to do: fetch a grammar, parse, choose a severity, and name the files constants belong in. That last one is the policy half and stays policy — beamte reports every declaration it finds,
const-filesdecides which are licensed.What the change costs and buys
n > MAX_SIZETOKEN_VAR=…inpublish.shThe lost eight languages are the honest price: a language going quiet here is now a grammar problem rather than a missing table entry, which is at least a problem with one fix. Both costs are why the rule is off by default twice over — a scan that reaches the network because the tool was upgraded is not a surprise anyone should get for free, and where constants belong is a decision no default can make.
rules/comments.rsreverts tomain. The code/comment masking it grew existed only to keep the regex off commented-out code; a parse does that for nothing.pack::cachedmoves out oftest_quality.rsintopack.rs, so both beamte hosts share one grammar cache rather than each keeping a private copy.Two decisions that keep it a rule rather than a nuisance
MAX_SIZEis the entire point of having it; flagging uses would make the rule unsatisfiable. This is the one the regex could not actually deliver.PI,OK,HTTP, a Go export and a C header guard are all single all-caps words — flagging them would bury the constants among them.One caveat worth knowing: an
enummember spelled in screaming snake is a binding outside a callable, so it reads as a declaration. That is the rule working as specified rather than a bug, andstraitjacket-allowcovers the cases where it isn't wanted.Dogfooding
Run against this repository with
const-files = ["src/config.rs"]: 24 findings across 43 files — 13 Rust, 6 shell, 5 TypeScript.MINIMUM_PACK_ABI,WASI_BADF,HEADER_LINE_LIMIT,LINE_MARKER,FILE_MARKER,STRUCTURED_CODEand the rest. Every one is a real declaration.The four above the regex version's twenty are shell reassignments in
publish.sh, which no tree can distinguish from declarations — shell has no separate declaration form. Spot-checked as correctly not flagged:use crate::language::{…, STRUCTURED_CODE}(a_directive), andhas_facet(&STRUCTURED_CODE)(a use, carrying no binding role at all).Running the tree version over
site/is also what caught a real bug in beamte before it shipped: TypeScript nestsvariable_declaratorinsidelexical_declarationand both carry_binding, somdxFiles(DOCS_DIR)came back as a declaration. Fixed upstream by reporting only the innermost binding.Verification
cargo fmt --check,cargo clippy --workspace --all-targets, 93 workspace tests (5 unit + 12 integration for this rule, covering 8 language cases), the 96 site docs-vs-manifest tests, the musl release build, vale on the prose, and straitjacket clean on itself.The integration tests fetch real packs, for the reason
tests/pack_host.rsgives at length: a test that passes because it found no grammar is worse than no test.Blocked on the same thing as #63
gatewill fail atscripts/publish.sh --dry-rununtil beamte is on crates.io.cargo publishignores[patch.crates-io]by design, so the dry run resolvesbeamte = "0.3"against the registry for real, where only 0.1.0 exists. The detail is on #63: beamte'sv0.2.0release run hit a 403 from an under-scoped crates.io token, which is a beamte settings fix rather than anything either PR can carry.The patch table here pins a commit, not a branch — a branch name stops resolving the moment it is merged and deleted, which is exactly how #63's CI broke under a PR that had not changed.
Merge order: beamte#23 → beamte 0.3 published → drop the patch table here.
Independent of #63 in code — the two share no source and can merge in either order. They are complementary:
env-varsgoverns where the environment may be read, this governs where constants may be declared, and a repository's env module is naturally named in both lists.🤖 Generated with Claude Code
https://claude.ai/code/session_01WrSzGnURZoupdEfVdwk9pg