Skip to content

fix(stella-observatory): clip the page's text on code points, not code units (#2026) - #2047

Merged
macanderson merged 2 commits into
mainfrom
fix-2026-page-clipper-surrogates
Aug 7, 2026
Merged

fix(stella-observatory): clip the page's text on code points, not code units (#2026)#2047
macanderson merged 2 commits into
mainfrom
fix-2026-page-clipper-surrogates

Conversation

@macanderson

@macanderson macanderson commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What & why

The dashboard's clip (src/assets/index.html) counted
String.prototype.length — UTF-16 code units — and cut with slice, so a
string whose n-th code unit fell inside a surrogate pair was cut into a lone
surrogate
and rendered as . It has ten call sites and the most exposed
carry user-authored text: prompts, session titles, recalled context, reflection
lessons.

It is the fourth copy of a clipper this crate consolidated in #1999, and the
only one that had actually drifted. That is not a coincidence — the page is
JavaScript inside a Rust crate, so rustc, clippy and the golden route payloads
all read straight past it. A duplicated decision that no gate can see is the
one that drifts.

clip now iterates code points via Array.from, matching db::truncate
exactly. The v.length <= n fast path is kept and is exact, not an
approximation
: a code point is one or two code units, so code units ≤ n
proves code points ≤ n. Only a string that might actually need clipping pays
for the array, so the common short case stays O(1) on a page that re-renders
every 5 s.

Closes #2026

The witness

  • This PR includes a witness test (fails on main, passes here)

tests/page_clipper.rs pulls clip out of the page as served by
respond
— not a copy, not a re-include_str! — runs it under node
against a corpus swept across every cut offset, and asserts agreement with
db::truncate's contract plus well-formed output.

Verified both directions by restoring the old one-liner in the working tree:

result
pre-fix clip FAILED — 35 mismatches
this PR ok. 4 passed

The first reported case is the defect in one line — got \ud83d…
(a lone high surrogate plus the ellipsis) against want 👍
(the intact 👍).

Two notes on how it is built:

  • Shelling out to node and skipping when absent is this workspace's
    established idiom for external binaries — stella-tools' git tests and
    registry/tests/fence.rs's make test both do it. Embedding a JS engine
    (boa, quickjs) is a large dependency for one arrow function. CI runners
    ship node, so it gates there.
  • A skipped test catches nothing, so code_unit_clipping_does_not_come_back
    asserts the shape of the extracted function and needs no node. A regression
    is never caught only by a test that did not run.

The driver escapes its diagnostics to bare ASCII, and that is load-bearing
rather than cosmetic: a lone surrogate is not representable in a Rust String,
so the first draft of this test failed on the old code with
unexpected end of hex escape — serde_json rejecting the driver's own output —
instead of naming the defect. The bug broke the channel used to report it.

The corpus also showed the defect was wider than surrogate splitting: with
any astral-plane character present, the old clipper cut at the wrong position
too, which is why 35 cases fail rather than a handful.

The gate

  • cargo fmt --check (via make guards-fast, exit 0)
  • cargo clippy -p stella-observatory --all-targets -- -D warnings (exit 0)
  • cargo test -p stella-observatory (exit 0 — 102 passed, 0 failed)
  • make guards-fast (exit 0), make doc-links (exit 0)
  • Docs updated — see below
  • CLA signed
  • Closes #2026 both here and as a commit trailer

Nothing left behind

  • There is nothing: everything I noticed is fixed in this PR.

Two things noticed and fixed here rather than deferred:

  1. The README's Testing section claimed "no tests/ directory" — three
    suites had already falsified that before this one. It now names all four and
    why each cannot be inline.
  2. A new Gotchas entry states the invariant itself: text is clipped twice, the
    two clippers must agree on shape, and only the lengths are per-surface.

Ground-rule check

  • No I/O added to stella-core; no new deps (node is invoked, not linked)
  • No new outbound network calls
  • The page stays fully inline — dashboard_html_has_no_external_references
    passes; nothing was imported

Anything reviewers should know?

Relationship to #2027. That PR (#1999) folds the three Rust clippers into
one db::truncate; this one fixes the JavaScript clipper. They are
independent — this branch is cut from main, touches no file #2027 touches,
and the two merge in either order.

Why the oracle is restated rather than imported. db::truncate is
pub(crate), so an integration test cannot call it, and the test states the
contract in six lines instead. That is what an oracle should be — an
independent derivation, since one sharing the implementation could not observe
a bug in it. The honest tradeoff: this pins the contract, so changing
db::truncate's behaviour fails here and forces the author to change both
deliberately. It does not silently follow.

Grapheme clusters are explicitly out of scope. A flag emoji or an
emoji-with-modifier is several scalar values in both languages, and both
clippers may still cut inside one. db::truncate does no grapheme
segmentation either, so matching it at the scalar level is exactly the
consistency being asked for; the corpus includes a combining mark and a ZWJ
sequence to pin that the two agree on this, rather than to claim it is
segmented. No segmentation library is pulled in.

Summary by Sourcery

Align the dashboard’s client-side text clipping with the server-side truncation to operate on Unicode code points instead of UTF-16 code units, and guard it with integration tests that execute the shipped HTML under Node.

Bug Fixes:

  • Fix client-side clipping of user-visible text that previously split surrogate pairs and produced lone surrogate replacement characters when truncating strings containing emoji or other astral characters.

Enhancements:

  • Document the shared clipping contract between server and page, and clarify the testing setup and external test suites in the observatory README.

Tests:

  • Add a page-clipper integration test that extracts the dashboard’s clip function from the served HTML, runs it under Node against a Unicode corpus, and asserts consistency with the server’s truncation contract while preventing regressions in clipping implementation.

…e units

The dashboard's `clip` counted `String.prototype.length` — UTF-16 code units —
and cut with `slice`, so a string whose n-th code unit fell inside a surrogate
pair was cut into a lone surrogate and rendered as `�`. It has ten call sites,
and the ones most exposed are user-authored text: prompts, session titles,
recalled context, reflection lessons.

It is the fourth copy of a clipper this crate consolidated in #1999, and the
only one that had actually drifted. That is not a coincidence: the page is
JavaScript inside a Rust crate, so rustc, clippy and the golden route payloads
all read straight past it. A duplicated decision no gate can see is the one
that drifts.

`clip` now iterates code points via `Array.from`, matching `db::truncate`
exactly. The `v.length <= n` fast path is kept and is exact rather than an
approximation — a code point is one or two code units, so code units <= n
proves code points <= n — which means only a string that might actually need
clipping pays for the array, and the common short case stays O(1) on a page
that re-renders every 5s.

The witness is tests/page_clipper.rs: it pulls `clip` out of the page as
served by `respond` (not a copy, not a re-`include_str!`), runs it under
`node` against a corpus swept across every cut offset, and asserts agreement
with `db::truncate`'s contract plus well-formed output. On the old code it
fails with 35 mismatches, the first reading got `\ud83d…` against want
`👍`; on the new code it passes. Shelling out to a binary and
skipping when absent is this workspace's idiom for this (stella-tools' git
tests, the registry fence test's `make`), and a shape assertion that needs no
node covers the skip case so a regression is never caught only by a test that
did not run.

The corpus also showed the defect was wider than surrogate splitting: with any
astral-plane character present the old clipper cut at the wrong position too.

README: the Testing section claimed "no `tests/` directory", which three
suites had already falsified before this one; it now names all four and why
each is not inline. A new Gotchas entry states the invariant — text is clipped
twice and the two clippers must agree on shape, only lengths are per-surface.

Closes #2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stella-cli-docs Ready Ready Preview Aug 7, 2026 5:46am

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Updates the dashboard’s JavaScript clipper to operate on Unicode code points instead of UTF-16 code units and adds a Node-driven integration test that extracts the actual served clipper from the HTML page to ensure it matches the Rust server-side truncation contract, alongside README documentation updates describing the dual clipping invariant and the external test suites.

Sequence diagram for Node-driven page clipper test

sequenceDiagram
    participant RustTest as page_clipper_rs
    participant Respond as respond
    participant Node as node
    participant JSClip as clip
    participant DbTrunc as db_truncate

    RustTest->>Respond: respond
    Respond-->>RustTest: index_html
    RustTest->>Node: execute_clip(index_html)
    Node->>JSClip: clip(s, n)
    JSClip-->>Node: clipped_js
    RustTest->>DbTrunc: db_truncate(s, n)
    DbTrunc-->>RustTest: clipped_db
    RustTest-->>RustTest: assert_eq(clipped_js, clipped_db)
Loading

File-Level Changes

Change Details Files
Make the dashboard’s JavaScript clipper operate on Unicode code points while keeping a fast path for short strings.
  • Replace the previous one-line clipper that used String length and slice (measuring UTF-16 code units) with a multi-line implementation that coerces input, checks v.length <= n as an exact non-clipping fast path, and otherwise iterates code points via Array.from.
  • Ensure clipped output uses code point-based slicing (Array.from(v).slice(0, n).join("")) and appends a single ellipsis character, matching db::truncate’s behaviour.
  • Document in code comments why code-point clipping is required, how it avoids lone surrogates, and why the fast path is safe and exact.
crates/stella-observatory/src/assets/index.html
Add an integration-style test suite that executes the actual served page’s clipper under Node and enforces behavioural and structural invariants.
  • Introduce tests/page_clipper.rs that calls stella_observatory::respond to obtain the served HTML, extracts the clip function by brace-counting from the const clip = definition, and uses Node to execute that function against a Rust-generated corpus.
  • Define an oracle function in Rust that restates db::truncate’s contract in terms of Unicode scalar values, and generate a corpus including ASCII, emoji, astral-plane characters, combining marks, ZWJ sequences, and non-string inputs to validate clipping behaviour and well-formedness.
  • Implement a Node driver script at test runtime that runs clip over all cases, checks for mismatches and lone surrogates (using String.prototype.isWellFormed when available or a regex fallback), escapes diagnostics to ASCII for robust JSON reporting, and fails the Rust test if any discrepancies are found.
  • Add additional Rust tests to ensure exactly one clip definition exists in the page, that the extractor captures the whole function with balanced braces, and that clip continues to use Array.from and does not regress to v.length > n code-unit clipping.
crates/stella-observatory/tests/page_clipper.rs
Update documentation to describe the dual text clippers, the new page-clipper gate, and the existing external test suites.
  • Extend the README’s gotchas section to explain that text is clipped twice (server db::truncate and page clip), that both must agree on code point-based clipping with an ellipsis, and that the page’s clipper is now tested via tests/page_clipper.rs executed under Node.
  • Correct and expand the README’s Testing section to acknowledge the presence of tests/ and to enumerate the four external test suites (schema_conformance, journal_era, live_stream, page_clipper) with brief explanations of why each must live in tests/ instead of inline tests.
crates/stella-observatory/README.md

Assessment against linked issues

Issue Objective Addressed Explanation
#2026 Update the clip function in crates/stella-observatory/src/assets/index.html so that it clips on Unicode scalar values (code points) instead of UTF-16 code units, thereby avoiding splitting surrogate pairs and emitting lone surrogates, while preserving existing clip lengths and behaviour otherwise.
#2026 Document in the crate’s README that the page’s JavaScript clipper and the server-side db::truncate are intentionally kept in agreement on clipping behaviour (both clip on code points and append ), and that this invariant is enforced.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@macanderson
macanderson merged commit 4e7947c into main Aug 7, 2026
12 of 13 checks passed
@macanderson
macanderson deleted the fix-2026-page-clipper-surrogates branch August 7, 2026 05:46
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.

stella-observatory: the page's JS clip splits surrogate pairs — the fourth copy of the clipper, already drifted from db::truncate

1 participant