Skip to content

amber-lsp on Crystal 1.21: pin ameba past the lexer drift, dogfood the FSDD rules, add a live-fire stdio smoke test - #31

Draft
crimson-knight wants to merge 6 commits into
mainfrom
fix/amber-lsp-crystal-1.21
Draft

amber-lsp on Crystal 1.21: pin ameba past the lexer drift, dogfood the FSDD rules, add a live-fire stdio smoke test#31
crimson-knight wants to merge 6 commits into
mainfrom
fix/amber-lsp-crystal-1.21

Conversation

@crimson-knight

@crimson-knight crimson-knight commented Aug 1, 2026

Copy link
Copy Markdown
Member

Draft. Makes amber-lsp build and its specs pass on Crystal 1.21, and adds a
live-fire proof that the built binary really speaks LSP.

Update: the two findings below started as "reported, not fixed". Both are
now fixed on this branch — the ameba dependency is pinned to the 1.7 dev line
(7761452), and the e2e fixture was corrected rather than the FSDD rules
relaxed, per the dogfood ruling (553c6bb).

Finding 1 — amber-lsp was never the thing that was broken

The reported symptom was that shards build amber-lsp fails on Crystal 1.21
with:

In src/ameba/tokenizer.cr:88:15
 88 | lexer.next_string_array_token
Error: undefined method 'next_string_array_token' for Crystal::Lexer

That call site is in ameba 1.6.4, our development_dependencies entry —
not in src/amber_lsp/. grep -rn next_string_array_token src/ spec/ returns
nothing. The failure happens during shards install's postinstall
(shards build -Dpreview_mt inside lib/ameba), before any of our source is
compiled. Skipping dev dependencies, the target built clean with zero source
changes
, which is how we know amber-lsp itself never drifted.

Crystal::Lexer#next_string_array_token exists in Crystal 1.20 and is gone in
1.21 — in both upstream crystal 1.21.0 and our agent-crystal build
(identical lexer sources); only next_string_array_token_noescape remains. So
it is genuine upstream Crystal drift, and it was ameba's to fix.

Fixed in 7761452. ameba fixed it in 4057e0c5e "Support Crystal v1.21.0+"
(2026-05-04), which guards the call:

token = if lexer.responds_to?(:next_string_array_token)
          lexer.next_string_array_token

That commit is reachable only from the untagged 1.7 line, so ~> 1.6.4 cannot
get there. Pinned by SHA (34a3de4, what v1.7.0-dev points at today) rather
than by that tag, because a -dev tag is a moving pointer.

before:  CRYSTAL=crystal-alpha shards build amber-lsp   -> exit 1
after:   CRYSTAL=crystal-alpha shards build amber-lsp   -> exit 0
         I: Installing ameba (1.7.0-dev at 34a3de4)

Two consequences worth knowing before merging.

  1. ameba 1.7.0-dev dropped both postinstall and its executables: list, so
    shards install no longer produces bin/ameba. Tooling that shells out to
    <root>/bin/ameba goes quiet until it is rebuilt, and shards build ameba
    does not do it (ameba is not a target of this shard.yml). Use:
    crystal-alpha build lib/ameba/src/cli.cr -o bin/ameba.
  2. Built that way, ameba 1.7.0-dev reports 636 findings across 225 files on
    this repo (there is no .ameba.yml, so it runs the full default rule set).
    How much of that is new in 1.7 versus pre-existing under 1.6.4 is not
    measurable on this toolchain
    — 1.6.4 cannot compile on Crystal 1.21 at all,
    which is the very bug being fixed here. A lint pass wants to be its own PR,
    with a .ameba.yml decision attached.

Finding 2 — the e2e fixture, and the dogfood ruling

agent_e2e_spec asserted that after the agent "fixes" the file, diagnostics are
empty. They were not: the fixed fixture drew 2x fsdd/doc-block-required and
1x fsdd/method-type-signature.

This was never Crystal 1.21 drift. git log puts it exactly: the spec last
changed in 06cdee5; the FSDD rules landed later in 8781d5b. The new rules
correctly fire on a fixture written before they existed.

That left a real product question — should the FSDD rules be on by default for
every Amber project? — with two possible answers: relax the rules, or fix the
fixture. Resolved in 553c6bb by the dogfood ruling: the rules stay on by
default; the fixture was wrong, not the rules.
The corrected code now carries
a doc comment on the class and on the action plus an explicit return type, so
"clean" in this spec means clean by our own conventions rather than merely free
of the two violations the fixture was written to demonstrate. Anything less and
the spec would quietly assert that our own conventions are optional.

253 examples, 0 failures, 0 errors, 0 pending

What this adds — scripts/lsp_smoke.rb

A live-fire proof that the built binary speaks LSP.

The spec/amber_lsp/ suite drives AmberLSP::Server in-process over an
IO::Memory pair. That proves the code, not the artifact — it cannot catch a
stale bin/amber-lsp, a broken build, or a server that goes silent instead of
answering. The script spawns the real executable against a throwaway
Amber-shaped fixture project and asserts on the publishDiagnostics
notifications that actually come back.

Three-valued exit on purpose: 0 pass, 1 expectations not met, 2
could-not-measure (no binary / handshake failure / timeout). A timeout is
never a pass
— a silent server is not a clean server.

$ /usr/bin/ruby scripts/lsp_smoke.rb
--- publishDiagnostics: VIOLATING fixture (src/controllers/users_controller.cr) ---
    warning L2 [amber/action-return-type] Action 'index' does not appear to call render, redirect_to, redirect_back, respond_with, or halt!
    error   L1 [amber/controller-naming]  Class 'UsersHandler' in controllers/ directory should end with 'Controller'
    info    L1 [fsdd/doc-block-required]  Class 'UsersHandler' is missing a doc comment
    info    L2 [fsdd/doc-block-required]  Method 'index' is missing a doc comment
    warning L2 [fsdd/method-type-signature] Method 'index' has incomplete type signature: missing return type
--- publishDiagnostics: CLEAN fixture (src/controllers/posts_controller.cr) ---
    (0 diagnostics)
LIVE-FIRE OK: violating=5 diagnostic(s), clean=0.

One trap the script documents in code, because it is a false-green generator:
ProjectContext.detect only enables diagnostics when shard.yml has an
amber dependency. Without one the server publishes nothing — and
"nothing" is indistinguishable from "clean" to any client that treats silence
as success.

Verified on

Crystal 1.21.0 [9c1e8ec64] (2026-07-20) via crystal-alpha.

Note on the commit range

This branch is cut from local HEAD (9f170cc), which was 3 commits ahead of
origin/main (a0ff687, 8781d5b, 9f170cc) — unpushed local work carried
along by the branch point. This PR's own changes are 549dd82, 7761452 and
553c6bb.

crimson-knight and others added 6 commits April 19, 2026 10:15
…manager structure, story grammar)

Four diagnostics rules enforcing the FSDD/Amber V2 standards: full method type
interfaces, mandatory doc blocks, process-manager structure (typed initialize + perform),
and feature-story grammar validity. Build 0/0, 44 specs pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parses crystal docs JSON (real signatures + FSDD doc-comment sections), applies a
version ramp, renders a self-contained static HTML site to FSDD_docs/html/. The shared
JSON layer the LSP also consumes. Build 0/0, 36 specs. (also: Time.monotonic→instant warning fix)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The spec/amber_lsp/ suite drives AmberLSP::Server in-process over an
IO::Memory pair. That proves the code but not the artifact: it cannot
catch a stale bin/amber-lsp, a broken build, or a server that goes silent
instead of answering.

scripts/lsp_smoke.rb spawns the real executable, builds a throwaway
Amber-shaped fixture project (shard.yml WITH an `amber` dependency --
ProjectContext.detect keeps the server silent without one), runs a real
framed initialize/initialized/didOpen session, and asserts on the
textDocument/publishDiagnostics notifications that come back: >= 1
diagnostic for the violating controller, exactly 0 for the clean one.

Exit codes are deliberately three-valued: 0 pass, 1 expectations not met,
2 could-not-measure (no binary, handshake failure, timeout). A timeout is
never reported as a pass -- a silent server is not a clean server.

Ruby 2.6 / stdlib only so it runs under macOS system ruby with no setup.

Verified against crystal-alpha 1.21.0 (Crystal 1.21.0 [9c1e8ec64]).
…rystal 1.21

Crystal 1.21 removed Crystal::Lexer#next_string_array_token. Every released
ameba up to 1.6.4 calls it unguarded from src/ameba/tokenizer.cr:88, so on
1.21 `shards install` dies inside ameba's own postinstall -- before a single
line of this project is compiled. The symptom looks like "amber-lsp does not
build on 1.21"; it is not. amber-lsp compiles clean and always did. The
failing target was ameba, a development dependency.

    Error: undefined method 'next_string_array_token' for Crystal::Lexer
    in lib/ameba/src/ameba/tokenizer.cr:88

ameba fixed it in 4057e0c5e "Support Crystal v1.21.0+" (2026-05-04), guarding
the call with `lexer.responds_to?(:next_string_array_token)`. That commit is
reachable only from the untagged 1.7 line, so `~> 1.6.4` cannot get there.
Pinned by SHA rather than by the `v1.7.0-dev` tag it currently points at,
because a `-dev` tag is a moving pointer. Switch to `~> 1.7` when 1.7.0 is
tagged.

Before:  CRYSTAL=crystal-alpha shards build amber-lsp  -> exit 1
After:   CRYSTAL=crystal-alpha shards build amber-lsp  -> exit 0
         I: Installing ameba (1.7.0-dev at 34a3de4)

KNOWN SIDE EFFECT: ameba 1.7.0-dev dropped both `postinstall` and its
`executables:` list, so `shards install` no longer produces bin/ameba. Any
tooling that shells out to <root>/bin/ameba (our fleet gate does, and skips
silently when it is absent) goes quiet until it is rebuilt. `shards build
ameba` does NOT do it -- ameba is not a target of this shard.yml. Use:

    crystal-alpha build lib/ameba/src/cli.cr -o bin/ameba
…d ruling)

agent_e2e_spec asserted that after the agent fixes the file, diagnostics are
empty. They were not: the fixture drew 2x fsdd/doc-block-required and 1x
fsdd/method-type-signature. This was never Crystal 1.21 drift -- the spec last
changed in 06cdee5 and the FSDD rules landed later in 8781d5b, so the new rules
correctly fire on a fixture written before they existed.

Two ways to make it green: turn the FSDD rules off by default, or fix the
fixture. Owner's standing directive decides it -- we need to be eating our own
dog food. The rules STAY ON BY DEFAULT; the fixture was wrong, not the rules.

So the corrected code now carries a doc comment on the class and on the action
and an explicit return type, and "clean" in this spec means clean by our own
conventions rather than merely free of the two violations the fixture was
originally written to demonstrate. Anything less and this spec would quietly
assert that our own conventions are optional.

    253 examples, 0 failures, 0 errors, 0 pending
@crimson-knight crimson-knight changed the title amber-lsp: live-fire stdio smoke test (+ the real Crystal 1.21 blocker is ameba, not amber-lsp) amber-lsp on Crystal 1.21: pin ameba past the lexer drift, dogfood the FSDD rules, add a live-fire stdio smoke test Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant