Skip to content

Add eval for preserving regex line splitting in stream refactors #47

Description

@martinfrancois

Problem

Add a Java Streams skill eval for behavior-preserving stream refactors where the old loop used a domain-specific splitter and the new stream source changed the parser contract.

This is eval-worthy because String.lines() looks like the natural stream replacement for looping over lines, but it is not behavior-equivalent to text.split("\\R", -1). String.lines() recognizes LF, CR, and CRLF. Java regex \R also recognizes linebreaks such as NEL, line separator, and paragraph separator.

Code before the prompt was executed

The pre-refactor code scanned each card text field and split logical lines with regex \R.

private static List<Declaration> declarations(Card card) {
    List<Declaration> declarations = new ArrayList<>();
    addDeclarations(declarations, card.title());
    addDeclarations(declarations, card.description());
    card.comments().stream().map(Card.Comment::text).forEach(text -> addDeclarations(declarations, text));
    return List.copyOf(declarations);
}

private static void addDeclarations(List<Declaration> declarations, String text) {
    if (text == null || text.isBlank()) {
        return;
    }
    for (String line : text.split("\\R", -1)) {
        Matcher labeled = LABELED_SOURCE.matcher(line);
        if (labeled.matches()) {
            declarations.add(new Declaration(labeled.group(2), labelMode(labeled.group(1))));
        }
    }
}

Prompt that caused the implementation

The maintainer asked for repeated Optional/Streams cleanup passes until actionable findings were gone:

ARE YOU SURE you covered all optioanls/streams thinfs ther3 are to cleanup with th3 skills? please do more passes until a pass comes back clean

This should have triggered the Streams skill to preserve parser semantics while refactoring the loop into a stream-producing shape.

Later prompt that exposed the issue

A later review found that the stream refactor was not behavior-preserving:

RepositorySourceResolver.java changed repository-source declaration parsing from text.split("\\R", -1) to text.lines(). That is not behavior-preserving. String.lines() recognizes only LF, CR, and CRLF, while regex \R recognizes Unicode linebreak sequences such as NEL \u0085, line separator \u2028, and paragraph separator \u2029.

Prompt-produced code before maintainer correction

The agent-produced stream code used String.lines() as the stream source.

private static List<Declaration> declarations(Card card) {
    return Stream.concat(
                    Stream.of(card.title(), card.description()),
                    card.comments().stream().map(Card.Comment::text))
            .flatMap(RepositorySourceResolver::declarations)
            .toList();
}

private static Stream<Declaration> declarations(String text) {
    if (text == null || text.isBlank()) {
        return Stream.empty();
    }
    return text.lines()
            .map(LABELED_SOURCE::matcher)
            .filter(Matcher::matches)
            .map(labeled -> new Declaration(labeled.group(2), labelMode(labeled.group(1))));
}

Why the prompt-produced code is bad

The code is not bad because it uses streams. It is bad because it silently changes the parser's definition of a logical line.

  1. The old parser recognized all Java regex \R linebreak sequences.
  2. String.lines() recognizes a narrower set of line terminators.
  3. A repository-source declaration after NEL, line separator, or paragraph separator can be missed.
  4. The refactor looks behavior-preserving because both APIs talk about lines, but they do not implement the same linebreak contract.
  5. This is a correctness issue, not only a style preference.

Maintainer-preferred code

Keep the stream-producing shape, but use a stream source that preserves the previous \R splitting semantics.

private static final Pattern LINE_BREAK = Pattern.compile("\\R");

private static List<Declaration> declarations(Card card) {
    return Stream.concat(
                    Stream.of(card.title(), card.description()),
                    card.comments().stream().map(Card.Comment::text))
            .flatMap(RepositorySourceResolver::declarations)
            .toList();
}

private static Stream<Declaration> declarations(String text) {
    if (text == null || text.isBlank()) {
        return Stream.empty();
    }
    return LINE_BREAK.splitAsStream(text)
            .map(LABELED_SOURCE::matcher)
            .filter(Matcher::matches)
            .map(labeled -> new Declaration(labeled.group(2), labelMode(labeled.group(1))));
}

Why the replacement is better

The replacement keeps the intended stream pipeline while preserving the old parser contract.

  • LINE_BREAK.splitAsStream(text) still produces a stream of logical lines.
  • The linebreak grammar stays the old regex \R grammar.
  • The mapping/filtering pipeline remains readable.
  • Regression tests can cover LF, CRLF, NEL, line separator, and paragraph separator without host assumptions.

Desired eval behavior

  • Reward checking whether a stream source is behavior-equivalent to the old loop source.
  • Reward preserving domain-specific parser splitting semantics when refactoring into streams.
  • Reward using Pattern.splitAsStream(...) when the old contract is regex-based splitting.
  • Reward explaining that String.lines() is not always equivalent to split("\\R", -1).
  • Reward adding regression tests for edge-case linebreaks when parser behavior changes.

Anti-patterns the eval should reject

  • Replacing split("\\R", -1) with String.lines() without auditing linebreak semantics.
  • Claiming both APIs are equivalent because both process "lines."
  • Dropping Unicode linebreak behavior during a behavior-preserving cleanup.
  • Reverting to the old imperative loop when a stream source can preserve the behavior directly.
  • Adding broad parser compatibility or new label semantics to hide the behavior change.

Suggested eval name

preserve-regex-line-splitting-in-stream-refactor

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions