↵ newline marker renders with no separating whitespace, so line-trailing values attach to the next line's identifier
The ↵ (U+21B5) hard-newline marker is rendered with no separating whitespace on either side, so the value ending one source line and the identifier starting the next fuse into a single visual token. A reader that transcribes the image perfectly still attributes line-trailing values to the following line.
This is a layout problem rather than a legibility one, which is why I'm filing it separately from the dense-render comprehension discussion in #160 — it is not fixed by a larger glyph cell or a better reader.
What the render looks like
Source (240 lines, one per record):
L00002 ledger entry alpha bravo charlie delta echo foxtrot golf hotel india juliet 00014
L00003 ledger entry alpha bravo charlie delta echo foxtrot golf hotel india juliet 00021Rendered strip, magnified 5x from the actual PNG:
…golf hotel india juliet 00014↵L00003 ledger entry alpha bravo…Note the spacing hierarchy, which is inverted with respect to structure:
| boundary | separation in the render |
|---|---|
| between words inside a line | 1 space |
| between a record ID and its content | 2 spaces |
between two source lines (↵) |
0 spaces |
The strongest boundary in the source gets the least visual separation of the three.
Consequence
00014↵L00003 reads as one compound token. In two independent reads at temperature: 0 the model transcribed the marker as / and reported seeing pairs of the form 00042/L00043 and 00042/L00021, then explained its answer in terms of that pairing — i.e. it treated the trailing number as belonging to the following identifier. Asked which five-digit number ends the line beginning L00042 (ground truth 00294), it answered 00047 and 00043. Both are adjacent-line artefacts, not random noise.
The marker itself is present and the legend is emitted correctly in the banner ("The glyph ↵ (U+21B5) marks an original hard line break in content; treat it as a real newline"), so the mechanism is all there. It is defeated by the kerning.
Why it may be worth more than its size suggests
For a coding agent this degrades gracefully — as noted in #160, the agent can fall back to grep when it cannot read something. But the failure here is silent: the read succeeds, it is simply off by one line. There is no signal that would prompt a fallback, and in a non-agentic pipeline (a plain API call with no filesystem) there is no fallback to reach for.
It only bites content where meaning lives in row alignment — tables, schedules, numbered clause lists, key/value dumps, log lines, code. Running prose is unaffected, since sentence boundaries survive reflow on punctuation alone.
Reproduction
lines = ["L%05d ledger entry alpha bravo charlie delta echo foxtrot golf hotel india juliet %05d"
% (i, i * 7 % 99991) for i in range(240)]
# send "\n".join(lines) as a system message through the proxy (~21.6k chars -> 1 image),
# then ask: "What five-digit number ends the line beginning L00042?" (truth: 00294)Observed on 0.13.2 via the OpenAI-compatible chat path; the rendered PNG is byte-identical on 0.13.0, so this is not a recent regression.
Suggested fix
Pad the marker with a space on each side (↵) so it separates at least as strongly as an intra-line word gap. That costs one or two cells per source line against the wrap budget, which seems cheap next to a systematic off-by-one in line attribution. markerScale in render.js already reserves extra wrap budget for an enlarged marker, so the surrounding whitespace may be the smaller change of the two.
Happy to test a patch against the same fixture if that would help.
Source: teamchong/pxpipe