Repository navigation
adds bend-lint tool - #1389
adds bend-lint tool#1389
Conversation
nicolas-abril
left a comment
There was a problem hiding this comment.
Thanks, this is careful work. The approach is sound for an opt-in tool, and it does what it says. On today's main the 48 tests pass and the branch merges cleanly. With no rules, a run costs about the same as bend --check-only (0.92 s vs 0.83 s on bench/checker/proofs_3200). A few things need fixing before it goes in, mostly around --fix and silent wrong results.
Reviewed at 48db83a. "Confirmed" below means reproduced on the branch; the rest are from reading the code.
Bugs
-
--fixcan rewrite cached BendHub packages (lint.ts:571). It writes safe fixes to every source that isn't Base, including imported packages cached under~/.bend/lib. bend only downloads a package when it's missing and never re-checks its hash, so every laterbendrun would silently use the edited package. Limit--fixto the root file, or at least skipBEND_LIBfiles. -
--fixaborts on identical edits (lint.ts:571, lint.ts:342). Confirmed. Two findings that insert the same" "at the same comma count as "overlapping". The run prints onlybend-lint: bend-lint: fixes are out of bounds or overlap(the prefix is doubled), exits 2, and shows no findings, becausefixedis computed before printing. This happens naturally with one typed rule that doesn't skipfact.inst, since a template instance repeats the body's spans. Merge identical edits, and skip conflicting fixes rather than aborting. -
Base facts leak into rules (lint.ts:251, lint.ts:253). Confirmed. The filter
bok.tlds[def]?.b !== truemisses constructor-field checks (theirdefis a constructor name, which isn't intlds) and Base template instances. Base is also only seeded when the root file matches/^import Base\s*$/m. Withimport Base # comment, or with Base reached through another import, 129 of 137 facts had spans inbase.bend. That contradicts "Base's own defs give no facts", and Base gets re-checked on every lint. -
Another
bend2checkout can get this one's code (lint.ts:506). Confirmed.Bun.pluginis process-wide, the filter only checks that the folder is namedbend2, and contents are looked up by file name alone. With bend-lint loaded, importing a different checkout'sbend2/bend.tsin the same process returned this checkout's patched code (its own exports were missing, and the patch marker was present). That's silently wrong code, not aDriftError. Filter on the full resolved path, and look up contents by full path. -
Drift that
DriftErrordoesn't catch:check()copies main.ts's privatebook_read/book_seed(lint.ts:245): the seeding, the PROOF.bend/LAWS.bend rule, thebook_validoffset and the TODO count. It has already diverged: it seeds on/^import Base\s*$/mwhere main.ts uses/^import Base$/m. It also leaves out the unsafe/foreign verdict. On a sample of 412 files fromtests/anddemos/, the 27 that rely on@unsafeor foreign code FAIL underbend --check-onlybut are "ok" under bend-lint, while the README says exit 1 means "the file does not check". Export a shared loader from bend2, or document the difference (or report it as a finding).- A future
comp.tsexport becomes a syntax error (patch.ts:133). Theneedscheck acceptsexport function js_sat, but the tail then addsexport { RUNTIME_MAIN, js_sat }. If comp.ts ever exportsjs_satitself, loading fails with a duplicate-export SyntaxError instead of a DriftError.
-
Rule validation misses an
undefinedentry (lint.ts:297).rules.findreturns the bad element itself, so anundefinedentry (or a hole) passes validation and then crashes inrules.some(...)with a generic TypeError. UsefindIndex. -
Identical files break span mapping (lint.ts:217).
mapperpicks the source by line content anddir, not by the span'sns. Two byte-identical files, or an import-error span with nodir, give two candidates, and the run dies with aDriftErrorinstead of reporting.
Performance
-
walkis quadratic (lint.ts:175). Confirmed. A recursive generator withyield*passes every node up through all its ancestors: O(nodes × depth). A 2,000-element list literal takes 408 ms and a 5,000-element one 2.56 s, with stack-overflow risk beyond that. An explicit stack would make it linear. -
Facts don't scale to big books. With a
needsTypesrule,proofs_3200records 1.28M facts and peaks at 4.4 GB (vs 1.05 GB for the check), taking 1.86 s vs 0.92 s. Each fact keeps its type, context and book alive, which pins checker state the GC would otherwise free. A Bend rule over those facts reaches 6.7 GB and 4.3 s, becauselint.jsbuilds a Bend list of every fact up front. Letting a rule narrow what it gets (the root file only, certain term kinds) would help, as would handing facts to Bend rules incrementally. -
Output is O(findings × file size) (lint.ts:379, lint.ts:364).
renderre-splits the whole file twice per fix, andpositionrebuilds the line table on every call (--jsoncalls it for every finding and edit).
Process
-
The PR description is stale. It describes
bend.pin,--pinandBEND_LINT_UNPINNED, which 00c86f4 removed; the README correctly says bend-lint "is not pinned". The test count is 48 now, not 45. -
Nothing runs these tests. The gates never run the suite, so the next change to
bend.ts's internals will only surface when someone next uses the tool. Adding its 0.6 s suite togates/test.tswould catch it. Longer term, a small official hook in bend.ts (an optionalbook.seecalled fromterm_infer/term_check) would remove the source patching entirely, but that's Taelin's call. -
It ships no rules. The only rules are the examples in the test file. If that's intended, say so; otherwise one or two real rules would make it useful out of the box.
I'd treat 1–5 as blockers. 8–10 can come later, and 12 is a decision.
🤖 Generated with Claude Code
|
Thanks for the review, all 13 points are handled. Short version: Blockers (1–5): all fixed
Other bugs (6–7): fixed
Performance (8–10)
Process (11–13)
Tests: 48 → 64, all pass. |
|
Working on 9. Please don't merge yet. |
|
Follow-up on #9 (memory with facts): fixed. Rules now say which facts they want. facts: { kinds: ["Var"] }, or by def or name. bend-lint drops every other fact as soon as the checker gives it to us, so they never pile up. |
f69aad0 to
ad0da00
Compare
bend-lint checks a file with bend's checker, then runs rule modules on it. Rules get the files on disk and, with needsTypes, the checker's result for each term (type, context, depth, def). Findings carry a severity, a code (the rule id) and fixes; the CLI prints them in bend's error layout and --fix applies the safe ones. bend2/ does not change. When bend-lint loads bend.ts it wraps term_infer and term_check to record results; it stops on a signature change, a failed self-check, a span it cannot map, or a bend.ts that differs from bend.pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
bend resolves imports with "/" paths and does not support native Windows, so the check fails there before bend-lint runs. The tool itself has no Windows-only failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
src/lint.ts holds the library and the CLI; src/patch.ts holds the two changes made to bend.ts as Bun loads it and the pin. The new change gives bend.ts an fs and path where real paths use "/" and a drive letter is a root, so relative imports resolve on Windows; on POSIX they act as node's. Single-use helpers are inlined, the term walk is a typed table, the CLI uses util.parseArgs, and the PatchError/SpanError pair is one DriftError. No re-exports. The tests and their fixtures are one file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
…emplate instance A template body is checked as written and again per instance, at the same spans. Instances are the names in book.tmps, so `fact.inst` needs no guessing from names; rules skip instance facts instead of relying on order. qt and us bring back what the first prototype recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
A file with its own `import Base` starts from a copy of a checked Base, as `bend --checkup` does; the copy is cached by the hash of base.bend. Facts of Base's own defs are not recorded, so typed rules see only the user's code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
A .bend rule imports src/lint.bend and defines id(), types(), run(input) and
a main that hands run to Lint.serve. bend-lint checks the rule once and, per
run, executes it with comp.ts io_run; lint.js's effects reach the host
through globalThis.BEND_LINT. Facts and types cross as indexes into the
run's tables (Fact{id}, Term{id}), and the rule asks the checker about them
(view, type_of, binder, same, show). bend.pin now also pins comp.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Lint.normal (a type's normal form), Lint.uses (the variables a term uses, with quantities) and Lint.text (the source under a span); Lint.View also carries how many times the term is demanded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
comp.ts is patched to export RUNTIME_MAIN and js_sat, so a Bend rule is compiled once (as io_run does) and each run only calls its main: about 10 ms per run instead of 100-150 ms. patch() takes the file it patches. RuleContext gains normal and uses, so the Bend channel only forwards to the canonical helpers. Removed: position() and line() (no caller; the LSP can bring them back), Diag.obs and Diag.note (no rule used them), the BEND_TS/COMP_TS constants and the slash export. One ternary less in check(); headers and README updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
The span mapper is typed with overloads instead of a generic cast, and the two bend2 imports are a typed tuple. tsc now reports no error in bend-lint (two remain in bend2/bend.ts, upstream). tsconfig.json was never committed; the README says to run tsc, since Bun does not check types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
--json prints only {ok, findings}, each with code, severity, message, def,
path, an LSP range (0-based, UTF-16) and fixes with their edits. position()
comes back for it, and line() is shared again with the span mapper.
--bend <dir> (read before bend loads) or $BEND_DIR picks a bend checkout
or its bend2 folder; the default is this repo's. The plugin matches that
folder, the pin is checked against it, and a wrong folder exits 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
The text edits to bend.ts are now two imports and two renames
(term_infer/term_check become unseen_*), plus a tail that defines
term_infer and term_check with seeInfer/seeCheck from patch.ts. Those are
plain TypeScript, checked by tsc against bend.ts's signatures, with named
parameters instead of copied signatures in strings. comp.ts only gets a
tail, `export { RUNTIME_MAIN, js_sat }`; patch() checks that each name it
exports is declared once. Hooked moves to patch.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
bend.ts and comp.ts change most days, and a pin broke bend-lint on every change, even ones it does not depend on. Now: - the wrappers pass every argument through, so bend computes what it would without them, and need the arity they read (f.length); - at load, every fact recorded for `x` in `def id(x: N) -> N: x` (one per wrapper) must have the right kinds of values, type, depth, scope, quantity, uses and span. Swapping two recorded arguments, or dropping the span, now fails with a DriftError naming what is wrong; - the exact edits and comp.ts's declared exports stay as they were. Removed: bend.pin, --pin, BEND_LINT_UNPINNED, current(), pinned(), and blob() (Base's cache is keyed by base.bend's text). With --bend on upstream main, which already differs from this branch's base, all tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Converting offsets between UTF-16 units and characters scanned the file from its start every time, so a typed Bend rule on a 1,917-line file took 18.7 s. A table per text, built once per run, makes it 0.2 s. The comma rule in the tests now walks the text with a tail call (bend runs those as loops); the old one overflowed the stack on large files. The README says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Nothing changes inside the bend repo: its own bend is still the default. Elsewhere, --bend or $BEND_DIR is required, and without them bend-lint stops with "no bend found". Type imports go through a `bend2/*` path alias in a self-contained tsconfig.json (typeRoots also look in the tool's own node_modules; @types/bun is a dev dependency). The drift tests read the chosen checkout's tests/ and skip any that are missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
A rule may declare options with defaults and read them from cx.options.
bend-lint.json (in the file's folder or above, or --config) sets them per
rule id, and can turn a rule "off" or set its severity. An unknown option,
a value of another type than its default, or a bad severity is an error.
lint() now takes { signal, config }.
Bend rules get their options in Lint.Input and read them with
Lint.option_number, option_flag and option_text, each with a default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
- --fix writes only the linted file, never an import or a cached BendHub
package. Equal edits merge; a fix that clashes with an earlier one is
skipped and counted, not fatal, and findings print first. A rule's own
fix with clashing or out-of-bounds edits is a rule error. applyFixes
returns { text, skipped }. Error messages no longer repeat "bend-lint:".
- Facts come only from the linted file: none from Base (constructor
fields, template instances) or other imports, seeded or not. Base is
seeded on main.ts's own /^import Base$/m.
- Bun.plugin matches this checkout's bend.ts and comp.ts by full path, so
another checkout in the process loads its own code.
- comp.ts: the tail exports only names comp.ts does not export already.
- An undefined entry in the rules is reported (findIndex).
- Spans map by namespace too, so identical files map; a failed check whose
span cannot map still reports.
- walk uses an explicit stack: 2,000 list elements took 408 ms, now ~5 ms.
- Line tables are built once per file; a fix's diff reads only its lines.
- The README says what exit 1 does not cover (@unsafe and foreign code),
and what facts cost on very large files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
bend-lint patches bend.ts in memory, so its tests are the first to see a change it relies on. The test gate now runs `bun test tools/bend-lint` (about 3 s) beside the cluster shards and counts it as one test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
rules/ holds ready rules, one per file, with shared.ts and shared.bend for helpers. None runs unless passed with --rules. - style/trailing-whitespace (TS): spaces or tabs at a line's end; safe fix. - style/line-length (Bend): a line past the option max (default 100); no fix. --fix-suggested also writes suggested fixes; --fix-dangerously writes all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
A rule's `facts` (a Bend rule's `facts()`) replaces needsTypes and types(): true for all of the linted file's facts, or a filter on scope (file, or program: imports too, never Base), kinds, defs and names. bend-lint keeps a fact only if some rule asks for it, and each rule gets only its own. On proofs_3200, `kinds: ["Var"]` peaks at 1.5 GB against 2.6 GB for all facts. A Bend rule now pulls facts one at a time (Lint.next_fact, or Lint.fold_facts) instead of getting one list of all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
- The hook reads a term's shape once, and only when some filter may want it; it no longer builds an array per term and filter. - open() had one caller left, so it is inline; the scope check of the per-rule fast path is computed once. - Test: a Bend rule without facts() is refused. - Docs: fold_facts in lint.bend's header, and a comment on fold_next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
With no --bend, no $BEND_DIR and no bend repo around it, bend-lint downloads one bend release from GitHub: the version of the `bend` on the PATH, else the newest release (from git's ref list, asked once a day). It takes bend.ts, comp.ts, base.bend and the effs/ files Base imports, into ~/.cache/bend-lint/<version>/bend2, through a temporary folder so a partial download is never used. Later runs, and offline runs, use the cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
bendDir gives bend2 paths with "/", as bend-lint does for every path it hands to bend; the test built the expected one with "\\" on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Its last case assumed the bend repo around tools/bend-lint, so it failed in the standalone repo; it now passes the repo as an option. BEND_DIR, when set, would win over every case, so the test unsets it and restores it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFwieXFxW49vQzucYb8Js4
Add the format/layout and style/file-length rules and their tests, move the effects to src/effects.js and the tests to src/lint.test.ts, and drop the old line_length, trailing_whitespace and shared.ts rules. Rewrite the README. Update the repo gate's allow list for the new file names. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoGeAfkpDBwvxzCtR9REJ5
lint(file, rules, { unsaved }) maps file paths to editor text that is not
saved. Bend and the rules read it in place of the file for that run only.
A run with unsaved text cannot overlap another run. This is what a language
server needs to check a buffer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoGeAfkpDBwvxzCtR9REJ5
9692f60 to
c81c42f
Compare
Sync with the standalone repo (263d99e). format/layout takes endOfLine: "lf" (default), "crlf" or "preserve". Preserve uses the first line ending, or LF if there is none. Line endings inside literals stay as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXvuzGKEyMUQ8ZTB1Z2Uqk
check() copied main.ts book_read and book_seed: Base seeding, the PROOF.bend/LAWS.bend rule and the TODO count. If bend changed them, bend-lint gave different results with no error. Now patch.ts exports book_read, book_err and Check_Fail from main.ts (with the same fs and path as bend.ts), and check() calls them. A download also gets main.ts and safe.ts; a cached release without main.ts downloads again. book_read makes its own book, so the wrappers report to hook.see in patch.ts, not to book.see. The hook and the unsaved text are global, so checks run one at a time in a queue. Runs with unsaved text now wait for each other instead of failing, and only one check's facts are in memory at a time. A failed check's message was only the expected type, without the names in scope. It is now "expected: ...\nobserved: ...", with the names in scope and bend's note, as `bend` prints it without the location. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lzp6cJXnt5wr1EMz1WAvoK
Synced from the standalone repo (MattCozendey/bend-lint, up to de52d65),
src/ and rules/ only:
- seam.ts replaces patch.ts and holds every call into bend2: finding and
downloading it, the in-memory patch, checking a file, the rule
operations, compiling a Bend rule. lint.ts and the rules import nothing
from bend2. At load, main.ts's book_read, book_err and Check_Fail are
checked, and a self-check runs src/sample.bend through check() and the
rule operations.
- Rules use bend-lint's own types: facts and types are handles, read with
cx.view, cx.type, cx.binder, cx.same, cx.show, cx.normal and cx.uses (the
interface Bend rules already had). A span is { file: Source, beg, end };
`spn` is now `span`. cx.sameDeclarations replaces format.ts's
sameProgram. cx.unstable and LintResult.unstable keep bend2's objects for
code that accepts to break when bend2 changes.
- The code is formatted with oxfmt.
gates/repo.ts allows seam.ts and sample.bend in place of patch.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lzp6cJXnt5wr1EMz1WAvoK
Synced from the standalone repo (up to bc054e4), src/ and the README. - Term view: cx.body(name), cx.node(fact), cx.shape(node) (kind with annotations kept, name, span, children) and cx.fact(node) (only facts this rule asked for). Bend rules get the same through lint.bend (Node, Shape, body, node, shape, fact). - DriftError is gone: drift errors are Errors named "DriftError" with the DRIFT symbol from seam.ts set to true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lzp6cJXnt5wr1EMz1WAvoK
compile() runs a Bend rule with `cli_args = args; return io_run(...)`,
which uses two names inside comp.ts's RUNTIME_MAIN. If bend renamed
io_run, each Bend rule failed on its first run with a ReferenceError; if
it renamed cli_args, the args were lost with no error. Now comp.ts's
patch needs `let cli_args = [];` and `function io_run(m) {` once each, or
loading stops with a drift error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lzp6cJXnt5wr1EMz1WAvoK
- layout() puts the finding's head in place of bend's "Error:" itself, so render() no longer knows how err_show's text starts. - compile() takes check()'s result, so bendRule() no longer holds bend's book. - bendRule's comment no longer describes comp.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lzp6cJXnt5wr1EMz1WAvoK
lint.bend and effects.js (the contract for Bend rules) and sample.bend (the self-check's program) move to src/bend/. A Bend rule now imports ../src/bend/lint.bend (rules/ and the test rules are updated). gates/repo.ts allows the three files in src/bend/ instead of src/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lzp6cJXnt5wr1EMz1WAvoK
The test gate no longer runs tools/bend-lint's suite: the tool is an external addition and must not block changes to bend2. repo.ts goes back to LF line endings; the only change is the four allow lines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- applyFixes: a fix that edits the linted file and another file is skipped whole instead of half applied; the CLI says how many. - --fix applies fixes even when a finding is an error (e.g. a rule made an error by the config); the exit code still says FAIL. - seam: sources take the text bend.ts read during the check, so a save while checking is no longer reported as bend drift. - seam: offline, latestTag uses the newest cached release without noting it, so the next run asks GitHub again. - seam: keep only the latest checked Base book instead of one per base.bend text ever seen. - seam: mapper splits each file's lines once, and only for files whose namespace and folder match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
I pushed two commits on top with fixes from review:
New tests cover the cross-file fix, Also, the PR description mentions a |
Add bend-lint, a standalone linter for Bend 2 with user-written rules
This moves the userland diagnostics work into a tool. Nothing in
bend2/ changes.
tools/bend-lintchecks a file with bend's own checker, then runs rules onit. A rule is a TS module or a Bend file. It reads the source text, the
checker's result for each term (type, scope, depth, quantity, uses), or
both, and returns findings with fixes. The CLI prints them in bend's error
layout, and
--fixapplies the safe fixes:How it gets the checker's results without changing bend: when bend-lint
loads
bend2/bend.ts(Bun only), it wrapsterm_inferandterm_checkinmemory so they record what they return, and gives bend.ts an fs and path
whose real paths use "/", so imports also resolve on Windows. For rules
written in Bend, it exports
RUNTIME_MAINandjs_satfrom comp.ts, so arule is compiled once. The files on disk do not change,
bendand thegates never load this, and checking gives the same results.
It does not drift silently. Each patch anchor must match exactly once, a
self-check runs at load, and
bend.pinholds the git blob hashes ofbend.ts and comp.ts. Any mismatch stops it with a DriftError. To bump:
BEND_LINT_UNPINNED=1 bun test tools/bend-lint, then--pin.It intentionally does not add anything to
bend's CLI, run in the gates,serve LSP, or give codes to bend's own errors (they are all
bend/check).The only file outside tools/bend-lint is gates/repo.ts: three allow lines.
Validation:
bun test tools/bend-lint(45 tests: patch anchors, span mapping, TS andBend rules, CLI, and 12 tests from tests/ against drift), on Linux, WSL
and Windows
bunx tsc -p tools/bend-lint/tsconfig.json --noEmit(no errors inbend-lint; the two it reports in bend2/bend.ts are on main too)
git diff --check