feat(validate): W-INVISIBLE-CHAR author lint for invisible characters (Closes #75)#76
Merged
Merged
Conversation
…Closes #75) Text pasted from a PDF or a web page carries characters that render as nothing: zero-width spaces, byte-order marks, directional marks, soft hyphens. They are legal JSON string content, so every structural check passes them, and nobody spots them by reading the file. This is not a theoretical path: the reference app's book-text wizard asks the author to paste a textbook section. The rule that prompted this was our own. The blog style guide audited invisible characters with grep -oP '[^\x00-\x7F]' <file> | sort -u # review every non-ASCII char which lists every umlaut and accent and asks a human to notice an invisible character among them. An invisible character is invisible in that output too. A check that names the codepoint is strictly better, and lesson content is where the paste actually lands. Detection lives in its own module. It walks every string in the lesson rather than a field list, which is what makes ext_payload covered without the engine knowing its shape, and keeps future schema fields covered for free. The warning names each distinct codepoint with its Unicode name, the occurrence count and the paths, aggregated once per lesson: pasted prose can carry dozens, and per-occurrence emission is the alert fatigue W-CARD-UNUSED was aggregated away from (#49). Deliberately NOT flagged: U+00A0 NO-BREAK SPACE and U+202F NARROW NO-BREAK SPACE. Both render as whitespace and are legitimate typography, notably in the French content this ecosystem carries ("Comment ca va ?" sets one before the question mark). Flagging them would make the lint usually-wrong, which is the over-sensitive behaviour it exists to avoid; two boundary tests pin the exclusion. TDD RED-first, 15 tests: one per codepoint class, ext_payload reach, per-lesson aggregation, and the silence cases (clean content, umlauts and accents, both no-break spaces, and that the lesson stays valid). Measured against real content rather than assumed: 528 lessons across eight content repositories produced 4 findings and 0 false positives. All four are genuine soft hyphens sitting mid-word in pasted prose ("Irrealit<AD>ät", "kultur<AD>spezi<AD>fische"), invisible to a reader and untouched until now. Those are content fixes in other repos, reported rather than silently changed. Proven to reach content repos without their cooperation: with this build overlaid as the engine dependency, the UNMODIFIED adaptive-learner-content-test gate emitted the warning through its existing `make lint-warnings`, and `make lint` still exited 0. Warning tier, additive, no schema change and no schema_version bump. Release 0.13.2 (patch), verified free on npm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
|
Content findings filed in the affected repositories, as agreed:
Five Nothing was changed in those repositories from here: the lint is the engine's job, the content is theirs. |
This was referenced Jul 22, 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.
Closes #75.
Why
Text pasted from a PDF or a web page carries characters that render as nothing: zero-width spaces, byte-order marks, directional marks, soft hyphens. They are legal JSON string content, so every structural check passes them, and nobody spots them by reading the file. The reference app's book-text wizard asks the author to paste a textbook section, so this is a live content path.
The rule that prompted it was our own. The blog style guide audited invisible characters with:
That lists every umlaut and accent and asks a human to notice an invisible character among them. An invisible character is invisible in that output too. Naming the codepoint is strictly better, and lesson content is where the paste actually lands.
What it does
W-INVISIBLE-CHAR, warning tier, never blocks. Example output:Detection sits in its own module and walks every string rather than a field list. That is what covers
ext_payloadwithout the engine knowing its shape, and covers future schema fields for free. Aggregated once per lesson, following theW-CARD-UNUSEDprecedent from #49: pasted prose can carry dozens, and per-occurrence emission is exactly the alert fatigue that aggregation solved.Deliberately not flagged:
U+00A0NO-BREAK SPACE andU+202FNARROW NO-BREAK SPACE. Both render as whitespace and are legitimate typography, notably in the French content this ecosystem carries ("Comment ca va ?" sets one before the question mark). Flagging them would make the lint usually-wrong, which is the over-sensitive third-party behaviour it exists to avoid. Two boundary tests pin the exclusion.Verification
TDD RED-first, 15 tests (10 red before the implementation): one per codepoint class,
ext_payloadreach, per-lesson aggregation, and the silence cases (clean content, umlauts and accents, both no-break spaces, lesson stays valid).Measured against real content, not assumed. 528 lessons across eight content repositories:
All four are genuine soft hyphens sitting mid-word in pasted prose:
Typesetting hyphenation points that survived a copy, invisible to a reader, sitting in the official content repos until now. Those are content fixes in other repos, so they are reported here rather than silently changed.
Reaches content repos without their cooperation. With this build overlaid as the engine dependency, the unmodified
adaptive-learner-content-testgate emitted the warning through its existingmake lint-warnings, andmake lintstill exited 0. No content repo has to change anything.Release
0.13.2, patch. Additive: warning tier only, no schema change, noschema_versionbump. Version verified free on npm before bumping.make release-checkgreen, 494 tests across 29 files.🤖 Generated with Claude Code