Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ as a design choice rather than a default optimization.
It also tells the agent to check the project Java version first. The right stream code for Java 8
may be different from the right code for Java 17, Java 21, or Java 24.

For general lambda, method-reference, identity-function, no-op functional stage, supplier-laziness,
and callback readability guidance, install the companion package
`martinfrancois/java-functional-style` together with this stream package.

## Contents

- [Getting Started](#getting-started)
Expand Down Expand Up @@ -234,6 +238,21 @@ Good fit:
- choosing Java-version-compatible APIs such as `takeWhile`, `mapMulti`, `Stream.toList()`, and
gatherers.

## Ownership Boundaries

`java-streams` owns stream and collector semantics: terminal operation choice, collector choice,
duplicate key and null behavior, encounter order, primitive streams, stream Java-version
compatibility, parallel stream behavior, gatherers, and stream-specific behavior preservation.

`java-functional-style` owns general Java lambda and functional-interface style: identity
functions, no-op functional stages, method references, callback readability, supplier laziness, and
callback side-effect boundaries.

`java-optionals` owns Optional semantics.

Install `java-streams` and `java-functional-style` together for stream cleanup involving non-trivial
callbacks.

Poor fit:

- broad Java style enforcement unrelated to streams or collectors;
Expand Down
42 changes: 42 additions & 0 deletions docs/agents/ownership-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Ownership Boundaries

`java-streams` owns stream and collector semantics:

- terminal operation choice
- collector choice
- duplicate key and null behavior
- encounter order
- primitive streams
- stream Java-version compatibility
- `findFirst` versus `findAny`
- `parallelStream`
- `Gatherers.mapConcurrent`
- stream-specific behavior preservation

`java-functional-style` owns general Java lambda and functional-interface style:

- method references
- identity functions
- no-op functional stages
- callback readability and helper extraction
- supplier laziness
- callback side-effect boundaries

`java-optionals` owns Optional semantics.

The expected high-quality setup for stream cleanup involving non-trivial callbacks is both
`java-streams` and `java-functional-style`.

Do not copy all generic lambda guidance back into `java-streams` to fix composition regressions.
Add only the smallest stream-side bridge instruction if hosted evidence proves it is required.

## Composition Gate

Before opening a PR for this split, existing stream evals must prove:

```text
current java-streams behavior <= slimmed java-streams + java-functional-style behavior
```

The comparison must use existing evals unchanged and criterion-level results must be equal or
better. Local validation alone is not enough.
7 changes: 4 additions & 3 deletions docs/agents/skill-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ guidance, or auto-selection wording.
that name.
- The skill should not force streams over clear stateful loops. Stateful sequence output, checked IO,
prompts, mutation-heavy code, or complex early exits can remain imperative.
- Keep stream lambdas as short glue. Prefer method references or one-expression lambdas whose body
stays on the same line as `->`, and extract named helpers for branching, loops, temporary
variables, formatting, merge rules, or nested stream chains that would continue on later lines.
- Generic lambda, method-reference, identity-function, no-op functional stage, supplier-laziness,
and callback readability guidance belongs to `java-functional-style`. Keep only stream and
collector semantics canonical here.
- Runtime guidance should keep internal workflow language out of ordinary user-facing reviews. Avoid
terms such as "hard stop", "marker", "scan", "checklist", and skill names unless the user asked
for an explicit skill workflow, audit, or scan command.
Expand All @@ -52,3 +52,4 @@ guidance, or auto-selection wording.

- [README Guidance](readme.md)
- [Eval Guidance](evals.md)
- [Ownership Boundaries](ownership-boundaries.md)
138 changes: 68 additions & 70 deletions skills/java-streams/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
---
name: java-streams
license: MIT
description: Review Java stream performance advice, especially slow stream mappings, external collection mutation with forEach/add, and whether parallelStream is safe; clean up mutation and write or refactor Java Stream and Collector code. Avoid common stream antipatterns such as materializing just to inspect, sorting before min/max, counting for existence, nested stream collections, unsafe null sorting, multi-line lambdas, and careless findFirst/findAny changes. Use whenever writing, reviewing, or refactoring Java code that uses Java streams, collectors, stream pipelines, grouping, joining strings, first/any element lookup, sorting, limiting, distinct values, primitive totals, Optional values in streams, or parallel streams, including review prompts asking whether a lookup should use findFirst or findAny.
description: "Review, write, and refactor Java Stream and Collector pipeline semantics: terminal choice, collector choice, duplicate and null behavior, encounter order, primitive streams, parallelStream safety, and Java-version compatibility. Use whenever Java code uses streams, collectors, stream pipelines, grouping, joining strings, first/any element lookup, sorting, limiting, distinct values, primitive totals, Optional values in streams, or parallel streams. Do not use for generic Java lambda or callback style unless it affects stream or collector behavior."
---

# Java Streams Skill

Preserve requested behavior, public API/artifact shape, encounter order, exceptions, null handling,
side effects, mutability, and Java-version compatibility. For implementation prompts, write the
requested Java source file before explaining. Keep provided helper/record/service types in that file,
and keep them nested when requested; do not create sibling source files, package-private top-level
replacements, hooks, test seams, delegate fields, overloads, caches, retries, or adapters unless
asked.
side effects, mutability, and Java-version compatibility. For implementation prompts, write only the
requested source; no extra public API, Javadoc, null guards, constructors, or utility ceremony unless
asked. Keep provided helper/record/service types in the requested file and nested when requested.

## Reference Bundle

Expand All @@ -20,93 +18,93 @@ asked.
| [hard-stops.md](references/hard-stops.md) | Replacement antipatterns and the marker scan to run |
| [stream-examples.md](references/stream-examples.md) | Worked before/after examples from the reference set |
| [java-stream-api.md](references/java-stream-api.md) | Java-version compatibility for stream and collector APIs |
| [review-output.md](references/review-output.md) | User-facing review wording for stream-specific decisions |

## Core Workflow

When the prompt asks for a named artifact such as `review.md` or a Java source file, create that
exact file. Do not answer only in chat when a file artifact is requested.

0. Check the Java baseline first. Use [java-stream-api.md](references/java-stream-api.md) for
minimum versions and fallbacks; do not emulate unavailable APIs with stateful or multi-line
lambdas.
minimum versions and fallbacks.
1. Identify the requested result and pick the matching terminal or collector:

| Goal | Preferred API |
|---|---|
| Arbitrary/equivalent match | `filter(...).findAny()` |
| First encounter-order match | `filter(...).findFirst()` |
| Existence check | `anyMatch` / `noneMatch` / `allMatch` |
| Transformed list/set | `map`/`filter` then collect |
| Concatenated text | `Collectors.joining` |
| Numeric primitive result | `mapToInt`/`mapToLong`/`mapToDouble` terminals |
| Two aggregates over same input (Java 12+) | `Collectors.teeing` |
| Grouping/indexing | `groupingBy`, `partitioningBy`, or `toMap` with merge/null handling |

Find-first rule: keep `findFirst()` when code takes element `0`, sorted order, or encounter order
chooses the winner. In these reviews, include the exception before performance claims: `findAny`
fits only if all matching values are equivalent and order does not choose the winner. Keep
`sorted(...).filter(...).findFirst()` when it defines the selected value; suggest `min`/`max` only
when it preserves that winner.
For first-match lookups, preserve order or priority with `findFirst()`; use `findAny()` only for
explicitly order-insensitive matches. In reviews that reject replacing an ordered lookup, include
the direct recommendation "Keep the existing `sorted(...).filter(...).findFirst()` chain." Do not
merely imply that recommendation. If natural ascending priority is used, say the lowest numeric
priority value wins, not "highest-priority." If filtered results are collected only to test
emptiness or read `get(0)`/`getFirst()`, recommend `.filter(...).findFirst().orElse(null)`.

2. Use intent-encoding terminals: `anyMatch`, `count`, `joining`, `min`/`max`, Java 12+
`teeing`, and primitive terminals. Do not mutate external containers, arrays, counters, or
builders from `forEach`; let the stream produce the result directly.

- Implementation prompts: for one-to-one transformations, write the direct stream result unless
mutable output or the Java baseline says otherwise; on Java 16+, prefer
- Direct transforms: write the direct stream result; on Java 16+, prefer
`names.stream().map(String::toUpperCase).toList()` over a manual `ArrayList` loop.
- External mutation/performance reviews: show a sequential result-producing snippet first. For
million-item CPU maps, mention benchmarking a pure parallel variant and include:
"`parallelStream()` can be slower for small lists or call paths that are usually small."
- Parallel reviews: apply [hard-stops.md](references/hard-stops.md); no custom pool snippets
unless asked.
- Java 24 bounded blocking calls: use `Gatherers.mapConcurrent(limit, item -> carrier(item,
stub(item)))`, then filter/map/sort; no test hooks, overloads, `CompletableFuture` fan-out, or
null sentinels.

```java
import java.util.List;

final class OrderChecks {
boolean hasOverdue(List<Order> orders) {
return orders.stream().anyMatch(Order::isOverdue);
}

record Order(boolean overdue) {
boolean isOverdue() {
return overdue;
}
}
}
```
3. Flatten nested sources deliberately. Use `flatMap`, `flatMap(Optional::stream)` on Java 9+,
and `mapMulti` on Java 16+ when clearer. Use helpers when nested lambdas would wrap. For subtype
primitives, filter/cast first, then call `mapToInt`/`mapToLong`/`mapToDouble` directly. For
nested collector callbacks, extract a named `Stream<T>` helper.

```java
// Java 9+: flatten Optional values instead of filter(Optional::isPresent).map(Optional::get)
optionals.stream().flatMap(Optional::stream).collect(Collectors.toList());
```
4. Choose accumulation/collectors by result semantics: use `reduce(identity, op)` for immutable
non-primitives, `toMap` with merge behavior and deliberate null-key/value handling, non-null keys
for `groupingBy`, `partitioningBy` for boolean splits, and flattened nested indexes when clearer.
Extract duplicate-key merge rules into named helpers when ties, nulls, or ordering need more than
a same-line expression. Carry `element + result`, never null sentinels.
5. Preserve ordering, mutability, short-circuit behavior, and lambda readability. Keep stream lambdas
as short glue or method references; use named helpers for branching, merge logic, or nested stream
work.
6. Keep imperative code when it is the clearer boundary for stateful output, checked IO,

```java
List<String> uppercaseNames(List<String> names) {
return names.stream().map(String::toUpperCase).toList();
}
```
- Performance/parallel reviews: show a sequential result-producing snippet first; use the review
output rule for benchmarking, overhead, and workload-size wording.
- Java 24 blocking calls: use bounded `Gatherers.mapConcurrent`; preserve element/result
association in a non-null carrier, call the existing helper/stub, filter by the service result,
and map back to the element. Do not use `parallelStream`, `ForkJoinPool`, futures, or test hooks;
use [stream-examples.md](references/stream-examples.md) for the exact shape.
3. Flatten nested sources deliberately. Use `flatMap`, Java 9+ `flatMap(Optional::stream)`, and
Java 16+ `mapMulti` when clearer. For subtype primitives, filter/cast first, then call
`mapToInt`/`mapToLong`/`mapToDouble`.

Extract nested `flatMap`, nested `anyMatch`, and downstream `Collectors.flatMapping` callbacks
recursively. Use stream-returning helpers or method references, not lambdas whose bodies continue
stream chains on later lines. See [stream-examples.md](references/stream-examples.md).
4. Choose accumulation/collectors by result semantics:
- immutable non-primitives: `reduce(identity, op)`
- duplicate keys or deliberate null behavior: `toMap` with explicit merge/null handling
- non-null grouping keys: `groupingBy`
- boolean splits: `partitioningBy`
- nested indexes: flattened collectors when clearer

Preserve duplicate-key merge rules, tie handling, null contracts, and map suppliers. Extract merge
helpers for branching, tie-breaking, or more than one comparison. Do not hide those rules in
multi-line ternaries or block merge lambdas. For simple cheapest/lowest-value merges, prefer
`BinaryOperator.minBy(Comparator.comparing(...))` over an inline ternary.
5. Keep imperative code when it is the clearer boundary for stateful output, checked IO,
mutation-heavy logic, or complex early exits.
7. Verify changed branches for empty inputs, one element, duplicates, nulls, ordering,
6. Verify changed branches for empty inputs, one element, duplicates, nulls, ordering,
parallel-safety, and baseline compatibility. Run the marker scan from
[hard-stops.md](references/hard-stops.md), fix hits, and re-scan. In scan audits, keep
hard-stop severities: required hits stay required unless explicitly acceptable.

Review output:

- Give a direct behavior-preserving decision plus one safe snippet.
- Create `review.md` when requested, even for rejection-only reviews.
- Explain code behavior, not internal workflow.
- Avoid internal workflow labels such as "per the skill", "hard stop", "marker", "scan",
"checklist", "rubric", or "criteria" unless the user asks about the workflow itself.
[hard-stops.md](references/hard-stops.md), fix hits, and re-scan. In scan audits:
- keep required hits required unless explicitly acceptable;
- give one compliant replacement, not a second marker violation;
- classify acceptable non-primitive reductions such as `BigDecimal.reduce(...)`;
- for Java-version-only audits, report only unavailable APIs and explicitly allowed markers, naming
each post-Java-8 API separately with [java-stream-api.md](references/java-stream-api.md).

Generic lambda and callback-style guidance belongs to the companion package
`martinfrancois/java-functional-style`. Install both packages when stream cleanup depends on callback
style. When both are available, keep stream callbacks as glue: extract local temporaries, branching,
date math, record construction, multi-condition filters, nested streams, and downstream
`Collectors.flatMapping` into named helpers or method references; do not leave block lambdas or
multi-line nested stream chains in the pipeline.

For `review.md`, lead with the technical decision and at most one safe snippet. Use
[review-output.md](references/review-output.md) for case-specific review wording, and never mention
skills, rules, rubrics, criteria, internal paths, or unrelated code issues. For ordered-lookup
rejections, make the safe chain an explicit recommendation, not just a description. Do not mention
`min`/`max` alternatives unless optimization advice is requested. For ascending numeric priority,
say the lowest priority number/value wins; never say "highest-priority" or call the sort no-op. When
the proposal uses `parallelStream().filter(...).findAny()`, separately state that `parallelStream()`
is unjustified without CPU-bound work or measurements and adds ordering/split/merge overhead. Do not
rely on "keep the original code" without naming the ordered chain.
Loading