Give a rule a terminator it can only know at match time - #23
Merged
Merged
Conversation
Merged in #22 in English only. The rule an integration is meant to follow — sync Core when you release, not when Core ships — reached half the readers, and the half it missed are the ones the Chinese doc exists for.
The Chinese table's last column was headed 状态 and said 已公开 for Core — a status, where the English column says where a user gets the thing today. The sentence directly underneath already explained the English column, so the Chinese page contradicted itself before it contradicted the translation.
The English versioning doc says @jsray/core is published from the jsray npm account; the Chinese one dropped the account and kept only the package name. Anyone reading it to find out who can publish came away without the answer.
Two things had gone stale in both languages at once, which is the failure a translation check cannot see. jsray-terminal is described as having a release carrying an installable build. It has no releases at all; the only way in is the GitHub install line, and a tag is already sitting there to cut one from. "Syncing them to 0.0.2-beta.2 is the next step" was written when Core was on beta.2 and now names neither the current release nor the rule: an integration syncs Core as part of its own release, so being behind between releases is the expected state, not a queued chore.
Every doc here is written twice, and nothing checked that the two agreed. A change landing in one language breaks no build and reads perfectly well — in the language you happen to be reading. #22 sat English-only for a day, and the Chinese repository table had been answering a different question than the English one for longer than that. The check compares what cannot legitimately differ between translations: versions, paths, package specifiers, links, section structure and table row counts. Prose, sentence counts and translated placeholders are left alone — <version> against <版本> is not drift. Presence is what it asserts, not counts, because a translation may merge or split sentences freely. Run against the two files as they stood before this branch, it reports both of the omissions above.
The table's last column is the only place in the docs quoting a full npm specifier, and it is what a reader copies. A parity check catches it going missing from one language; nothing catches both languages naming the same stale release, which is the shape every other entry in this file was added for.
"Revisit at public beta" named 2026-07-17. That date passed, nothing happened, and the entry became something every planning round rediscovers and re-argues from scratch. The minify question now carries its answer — a readable, auditable dist is worth more than the 9 KB brotli saves, measured — and a condition for reopening it that cannot silently expire: a real size complaint, or Core growing substantially. Detection tuning said it "belongs with the 0.0.2 engine work" while we are already inside 0.0.2. What it actually needs is a beta round of its own, because retuning the scores reorders all 83 grammars at once and nothing else in the same round would be separable from it. The third of these — string rules waiting on "the API pass in 0.0.2" — is left alone here because the work itself is what rewrites that entry.
Six commits landed without a changelog entry between them: the parity check, the translations it was built to catch, and the two roadmap deadlines that were rewritten as conditions. The audits that keep finding unrecorded batches would have found this one.
Heredocs, %w[…], q{…} and sigils rendered as ordinary code, and not for want
of a pattern: their end is genuinely not knowable when the rule is written.
`<<<SQL` ends at SQL and `<<<EOT` at EOT — the same rule, two different
terminators, chosen by the text being highlighted. A RegExp cannot express
that, which is why five languages had a hole in the same shape.
A rule may now carry `close(match, text, from)`, called after its pattern
matches an opening, returning where the form ends. Two builders cover every
case the grammars needed: one derives a heredoc terminator from the capture
holding its name, the other walks to the bracket matching the opener,
counting depth where brackets nest and not counting it where a symmetric
delimiter cannot.
This is additive. `GrammarRule` gains an optional field and every existing
rule keeps working untouched, so the public type did not have to break for
it — the migration of the hand-written string rules onto one builder, which
does break it, stays a round of its own.
Three decisions worth keeping:
An opening with no terminator reports no match rather than running to the end
of input. A stray `<<` in `$(( a << 2 ))` is likelier than a real
unterminated heredoc, and the failure modes are not comparable: one leaves a
shift operator uncoloured, the other paints the rest of the file as a string.
Ruby opens a heredoc only on an uppercase word, because `items << thing` is
the append operator and shares the token exactly.
The shell opening line is consumed as an uncoloured prefix, so the redirect
in `cat <<EOF > out.txt` stays shell instead of being dragged into the
literal.
Eleven cases in tests/constructs.test.mjs, including the openings that must
be declined and four unterminated-opening storms in the backtracking guard —
these scan forward, so a file of nothing but openings is the shape that would
expose a quadratic one.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
jsray | 3b6fd0f | Commit Preview URL Branch Preview URL |
Sep 07 2026, 10:12 PM |
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nine commits: the engine change that lets a rule find its own end, five languages that needed it, and the documentation batch that accumulated ahead of it.
The engine
Heredocs,
%w[…],q{…}and Elixir sigils rendered as ordinary code, and not for want of someone writing a pattern: their end is genuinely not knowable when the rule is written.<<<SQLends at SQL and<<<EOTat EOT — one rule, two terminators, chosen by the text being highlighted.A rule may now carry
close(match, text, from), called after its pattern matches an opening, returning where the whole form ends. Two builders cover every case the grammars needed: one derives a heredoc terminator from the capture holding its name, the other walks to the bracket matching the opener — counting depth where brackets nest, and not counting it where a symmetric delimiter cannot.This is additive.
GrammarRulegains an optional field and every existing rule keeps working untouched, so the public type did not have to break for it. Migrating the hand-written string rules onto one builder — which does break it — stays a round of its own inside the 0.0.x window.Three decisions worth reviewing
<<in$(( a << 2 ))is likelier than a real unterminated heredoc, and the failure modes are not comparable: one leaves a shift operator uncoloured, the other paints the rest of the file as a string.items << thingis the append operator and shares the token exactly.cat <<EOF > out.txtstays shell rather than being dragged into the literal.The documentation batch
check:docs-paritycompares what cannot legitimately differ between a document and its translation — versions, paths, package specifiers, links, section structure, table row counts — and leaves prose and translated placeholders alone. Run against the tree before this branch, it reports the two omissions it was written for.It also carries the four translation gaps it was built to catch, the roadmap's claim that jsray-terminal has a release (it has none), and two deadlines rewritten as conditions: "revisit at public beta" named 2026-07-17, and kept being rediscovered as an open question every planning round after that date passed. Minification now carries its answer and a trigger that cannot expire.
Verification
175 tests pass, up from 164. The new cases include the openings that must be declined, and four unterminated-opening storms added to the backtracking guard — these scan forward, so a file of nothing but openings is the shape that would expose a quadratic one.
check:integrity,check:versionsandcheck:docs-parityare green. No version bump: this accumulates under[Unreleased].