-
+
diff --git a/Writerside/writerside.cfg b/Writerside/writerside.cfg
index 417a590..7886df6 100644
--- a/Writerside/writerside.cfg
+++ b/Writerside/writerside.cfg
@@ -9,5 +9,5 @@
-
+
diff --git a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java
index 599c712..b95cb5e 100644
--- a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java
+++ b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java
@@ -98,7 +98,7 @@
public final class WeaverAgent {
/** The version this agent prints on its startup line. */
- private static final String VERSION = "0.1.0";
+ private static final String VERSION = "0.1.1";
/**
* Refuses instantiation.
diff --git a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java
index 8c9649f..7fa7d00 100644
--- a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java
+++ b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java
@@ -135,7 +135,11 @@ final class WeavingTransformer implements ClassFileTransformer {
return null;
}
try {
- final byte[] woven = this.weaver.weave(className, buffer);
+ // The loader that defines this class is what lets the engine resolve the application's
+ // own types through their class-file resources when it regenerates stack maps. Dropping
+ // it here forces the frame builder onto a resolver that reflects over the system loader
+ // and cannot see host classes, which fails to verify any target whose frames merge them.
+ final byte[] woven = this.weaver.weave(className, buffer, loader);
if (woven == null) {
// Contract point 1. Returning `buffer` here would be correct-looking and would make
// the JVM re-verify every class in the application.
diff --git a/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java b/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java
index 0082dd9..54eccf6 100644
--- a/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java
+++ b/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java
@@ -65,7 +65,7 @@ void theStartupLineIsPrinted() throws Exception {
assertThat(result.output())
.as("a run whose configuration is not visible in its own log is a run whose "
+ "behaviour has to be reconstructed from the deployment")
- .contains("Aether Weaver 0.1.0")
+ .contains("Aether Weaver 0.1.1")
.contains("1 weave")
.contains("fingerprint ")
.contains("verification=strict");
diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java
index 8c7019e..55fe657 100644
--- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java
+++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java
@@ -238,7 +238,7 @@ private DiagnosticListener onceFor(@NotNull final String key,
}
/** The weaver version, written into every weave record and printed by the explain report. */
- static final String VERSION = "0.1.0";
+ static final String VERSION = "0.1.1";
/**
* Every kind this release declares, in one place.
@@ -300,6 +300,28 @@ public static WeaverBuilder builder() {
*/
public byte @Nullable [] weave(@NotNull final String internalName,
final byte @NotNull [] original) {
+ return weave(internalName, original, null);
+ }
+
+ /**
+ * Weaves a class whose bytes are already in hand, resolving its type hierarchy through the loader
+ * that defines it.
+ *
+ * The loader is what lets load-time stack-map generation resolve the application's own types
+ * through their class-file resources without loading them. {@code null} stands for the bootstrap
+ * loader, for which the platform loader is used, exactly as an agent is handed {@code null} for a
+ * class the bootstrap loader defined.
+ *
+ * @param internalName the class's internal name, such as {@code com/acme/Ledger}
+ * @param original the class as it stands; not modified
+ * @param loader the loader that defines the class, whose resources describe the hierarchy,
+ * or {@code null} for the bootstrap loader
+ * @return the woven class, or {@code null} when it is to be used unchanged
+ * @throws NullPointerException if {@code internalName} or {@code original} is {@code null}
+ */
+ public byte @Nullable [] weave(@NotNull final String internalName,
+ final byte @NotNull [] original,
+ @Nullable final ClassLoader loader) {
Objects.requireNonNull(internalName, "internalName");
Objects.requireNonNull(original, "original");
this.statistics.seen();
@@ -307,9 +329,9 @@ public static WeaverBuilder builder() {
final List entries = this.plan.entriesFor(internalName);
final List dissolving = this.plan.structuralFor(internalName);
if (entries.isEmpty() && dissolving.isEmpty()) {
- return extensionsOnly(internalName, original);
+ return extensionsOnly(internalName, original, loader);
}
- return timed(internalName, () -> original, entries, dissolving);
+ return timed(internalName, () -> original, entries, dissolving, loader);
}
/**
@@ -335,10 +357,10 @@ public static WeaverBuilder builder() {
final List entries = this.plan.entriesFor(internalName);
final List dissolving = this.plan.structuralFor(internalName);
if (entries.isEmpty() && dissolving.isEmpty()) {
- return this.extensions.isEmpty() ? null : extensionsOnly(internalName, original.get());
+ return this.extensions.isEmpty() ? null : extensionsOnly(internalName, original.get(), null);
}
- return timed(internalName, original, entries, dissolving);
+ return timed(internalName, original, entries, dissolving, null);
}
/**
@@ -353,12 +375,14 @@ public static WeaverBuilder builder() {
*
* @param internalName the class's internal name
* @param original the class as it stands
+ * @param loader the loader that defines the class, or {@code null} for the bootstrap loader
* @return the rewritten class, or {@code null} when neither rewrite applied; also the original
* bytes, unrewritten, when a rewrite applied but the verifier refused the result under
* {@code REPORT}
*/
private byte @Nullable [] extensionsOnly(@NotNull final String internalName,
- final byte @NotNull [] original) {
+ final byte @NotNull [] original,
+ @Nullable final ClassLoader loader) {
if (this.extensions.isEmpty()) {
return null;
}
@@ -373,7 +397,7 @@ public static WeaverBuilder builder() {
// the original, and a caller that wrote the returned bytes unconditionally still gets a
// class that loads.
return this.verifier.check(internalName, original,
- guarded == null ? rewritten : guarded);
+ guarded == null ? rewritten : guarded, loader);
}
/**
@@ -386,15 +410,17 @@ public static WeaverBuilder builder() {
* @param original supplies the class as it stands
* @param entries the injections planned for it
* @param dissolving the weaves to merge into it
+ * @param loader the loader that defines the class, or {@code null} for the bootstrap loader
* @return whatever {@link #apply} returned
*/
private byte @Nullable [] timed(@NotNull final String internalName,
@NotNull final ByteSupplier original,
@NotNull final List entries,
- @NotNull final List dissolving) {
+ @NotNull final List dissolving,
+ @Nullable final ClassLoader loader) {
final long started = System.nanoTime();
try {
- return apply(internalName, original, entries, dissolving, started);
+ return apply(internalName, original, entries, dissolving, started, loader);
} finally {
this.statistics.spent(System.nanoTime() - started);
}
@@ -428,6 +454,7 @@ public static WeaverBuilder builder() {
* @param entries the injections planned for it
* @param dissolving the weaves to merge into it
* @param started when timing began, for the JFR event
+ * @param loader the loader that defines the class, or {@code null} for the bootstrap loader
* @return the woven class, {@code null} when it is to be used unchanged, or the original bytes
* when a verifier refusal under {@code REPORT} left {@code checked != woven}
*/
@@ -435,7 +462,8 @@ public static WeaverBuilder builder() {
@NotNull final ByteSupplier original,
@NotNull final List entries,
@NotNull final List dissolving,
- final long started) {
+ final long started,
+ @Nullable final ClassLoader loader) {
final byte[] bytes = original.get();
final ClassModel model = ClassFile.of().parse(bytes);
@@ -478,7 +506,7 @@ public static WeaverBuilder builder() {
final Reporter reporter = this.listener::report;
byte[] current = bytes;
if (!entries.isEmpty()) {
- final byte[] woven = this.pipeline.weave(model, entries, this.groups, reporter);
+ final byte[] woven = this.pipeline.weave(model, entries, this.groups, reporter, loader);
if (woven == null) {
// Nothing resolved, or accounting refused it. The pipeline has already said why,
// and the original class stands — never a half-woven one, because the pipeline
@@ -489,7 +517,7 @@ public static WeaverBuilder builder() {
}
if (!dissolving.isEmpty()) {
final byte[] merged = this.structural.apply(
- ClassFile.of().parse(current), dissolving, reporter);
+ ClassFile.of().parse(current), dissolving, reporter, loader);
if (merged == null && current == bytes) {
return null;
}
@@ -520,7 +548,7 @@ public static WeaverBuilder builder() {
// Gate 5: verify. Under REPORT this hands back the ORIGINAL, never the broken class.
final byte[] woven = current;
- final byte[] checked = this.verifier.check(internalName, bytes, woven);
+ final byte[] checked = this.verifier.check(internalName, bytes, woven, loader);
if (checked != woven) {
// Counted, not returned as a success. The class the caller gets back is the original
// one, so a run that reported "42 classes woven" while a verifier had quietly handed
diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java
index e2ceb1f..f4951e5 100644
--- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java
+++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java
@@ -42,7 +42,7 @@ public final class CorePlugin implements WeaverPlugin {
/** Identifies the plugin. The version is a literal here and is not read from the build. */
private static final PluginId ID = new PluginId(
- PluginId.BUILT_IN_NAMESPACE, "Aether Weaver", "0.1.0");
+ PluginId.BUILT_IN_NAMESPACE, "Aether Weaver", "0.1.1");
/**
* Creates the plugin.
diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java
index 1afc2c9..f3d4041 100644
--- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java
+++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java
@@ -25,12 +25,12 @@
import de.splatgames.aether.weaver.engine.inject.point.ModelViews;
import de.splatgames.aether.weaver.engine.inject.point.PointResolver;
import de.splatgames.aether.weaver.engine.inject.point.Targets;
+import de.splatgames.aether.weaver.engine.internal.transform.FrameSupport;
import de.splatgames.aether.weaver.engine.internal.transform.LocalTable;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.ClassTransform;
import java.lang.classfile.CodeElement;
@@ -105,6 +105,28 @@ public WeavingPipeline(@NotNull final Function points,
this.observer = Objects.requireNonNull(observer, "observer");
}
+ /**
+ * Weaves one class, resolving its type hierarchy through the bootstrap loader.
+ *
+ * Equivalent to {@link #weave(ClassModel, List, List, Reporter, ClassLoader)} with a
+ * {@code null} loader: a caller with no defining loader to offer, such as a build-time driver,
+ * lets stack-map generation resolve through class-file resources on the bootstrap loader.
+ *
+ * @param model the class to weave; must not be {@code null}
+ * @param entries the declarations to apply, in the order they should be offered each element;
+ * must not be {@code null}
+ * @param groups the groups the entries are accounted against; must not be {@code null}
+ * @param reporter where to report; must not be {@code null}
+ * @return the woven bytes, or {@code null} when the class was not woven
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public byte @Nullable [] weave(@NotNull final ClassModel model,
+ @NotNull final List entries,
+ @NotNull final List groups,
+ @NotNull final Reporter reporter) {
+ return weave(model, entries, groups, reporter, null);
+ }
+
/**
* Weaves one class.
*
@@ -128,13 +150,16 @@ public WeavingPipeline(@NotNull final Function points,
* must not be {@code null}
* @param groups the groups the entries are accounted against; must not be {@code null}
* @param reporter where to report; must not be {@code null}
+ * @param loader the loader that defines the target, whose resources describe the hierarchy the
+ * stack-map generator merges over; {@code null} for the bootstrap loader
* @return the woven bytes, or {@code null} when the class was not woven
- * @throws NullPointerException if any argument is {@code null}
+ * @throws NullPointerException if any argument but {@code loader} is {@code null}
*/
public byte @Nullable [] weave(@NotNull final ClassModel model,
@NotNull final List entries,
@NotNull final List groups,
- @NotNull final Reporter reporter) {
+ @NotNull final Reporter reporter,
+ @Nullable final ClassLoader loader) {
Objects.requireNonNull(model, "model");
Objects.requireNonNull(entries, "entries");
Objects.requireNonNull(groups, "groups");
@@ -226,10 +251,10 @@ public WeavingPipeline(@NotNull final Function points,
final String contributed = contributedKinds(byMethod);
if (contributed.isEmpty()) {
- return emit(model, internalName, byMethod, reporter);
+ return emit(model, internalName, byMethod, reporter, loader);
}
return PluginIsolation.call(contributed, PluginIsolation.Phase.APPLY, reporter,
- () -> emit(model, internalName, byMethod, reporter)).orElse(null);
+ () -> emit(model, internalName, byMethod, reporter, loader)).orElse(null);
}
/**
@@ -247,12 +272,15 @@ public WeavingPipeline(@NotNull final Function points,
* @param byMethod the declarations to apply, grouped by target method name; must not be
* {@code null}
* @param reporter where to report; must not be {@code null}
+ * @param loader the loader that defines the target, whose resources describe the hierarchy,
+ * or {@code null} for the bootstrap loader
* @return the woven bytes, or {@code null} when the writer refused the result
*/
private byte @Nullable [] emit(@NotNull final ClassModel model,
@NotNull final String internalName,
@NotNull final Map> byMethod,
- @NotNull final Reporter reporter) {
+ @NotNull final Reporter reporter,
+ @Nullable final ClassLoader loader) {
ClassTransform transform = ClassTransform.ACCEPT_ALL;
for (final Map.Entry> group : byMethod.entrySet()) {
final String methodName = group.getKey();
@@ -265,7 +293,7 @@ public WeavingPipeline(@NotNull final Function points,
CodeTransform.ofStateful(() -> compose(emissions, ranges))));
}
try {
- return ClassFile.of().transformClass(model, transform);
+ return FrameSupport.forLoadTime(loader).transformClass(model, transform);
} catch (final IllegalArgumentException refused) {
// The Class-File API validates while it writes, so the one limit an injection can
// realistically cross — 65535 bytes of code in one method — surfaces here as an
diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java
index 9ce8ecc..bff9b51 100644
--- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java
+++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java
@@ -6,6 +6,7 @@
import de.splatgames.aether.weaver.api.model.HandlerRef;
import de.splatgames.aether.weaver.api.spi.Reporter;
import de.splatgames.aether.weaver.engine.internal.transform.ClassRemapper;
+import de.splatgames.aether.weaver.engine.internal.transform.FrameSupport;
import de.splatgames.aether.weaver.engine.model.WeaveClass;
import de.splatgames.aether.weaver.engine.model.WeaveMember;
import org.jetbrains.annotations.NotNull;
@@ -63,6 +64,27 @@ public StructuralWeaver(@NotNull final WeaveBytes bytes) {
this.bytes = Objects.requireNonNull(bytes, "bytes");
}
+ /**
+ * Applies every weave that dissolves into the given class, resolving the hierarchy through the
+ * bootstrap loader.
+ *
+ * Equivalent to {@link #apply(ClassModel, List, Reporter, ClassLoader)} with a {@code null}
+ * loader, which is what a caller with no defining loader to offer, such as a build-time driver,
+ * passes.
+ *
+ * @param model the class to weave, as it stands; must not be {@code null}
+ * @param weaves the weaves that dissolve into it; must not be {@code null}
+ * @param reporter where refusals are reported; must not be {@code null}
+ * @return the rebuilt class, or {@code null} when a weave was refused or there was nothing to
+ * emit
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public byte @Nullable [] apply(@NotNull final ClassModel model,
+ @NotNull final List weaves,
+ @NotNull final Reporter reporter) {
+ return apply(model, weaves, reporter, null);
+ }
+
/**
* Applies every weave that dissolves into the given class.
*
@@ -75,13 +97,16 @@ public StructuralWeaver(@NotNull final WeaveBytes bytes) {
* @param model the class to weave, as it stands; must not be {@code null}
* @param weaves the weaves that dissolve into it; must not be {@code null}
* @param reporter where refusals are reported; must not be {@code null}
+ * @param loader the loader that defines the target, whose resources describe the hierarchy the
+ * stack-map generator merges over; {@code null} for the bootstrap loader
* @return the rebuilt class, or {@code null} when a weave was refused or there was nothing to
* emit
- * @throws NullPointerException if any argument is {@code null}
+ * @throws NullPointerException if any argument but {@code loader} is {@code null}
*/
public byte @Nullable [] apply(@NotNull final ClassModel model,
@NotNull final List weaves,
- @NotNull final Reporter reporter) {
+ @NotNull final Reporter reporter,
+ @Nullable final ClassLoader loader) {
Objects.requireNonNull(model, "model");
Objects.requireNonNull(weaves, "weaves");
Objects.requireNonNull(reporter, "reporter");
@@ -103,7 +128,7 @@ public StructuralWeaver(@NotNull final WeaveBytes bytes) {
return null;
}
- return ClassFile.of().build(model.thisClass().asSymbol(), builder -> {
+ return FrameSupport.forLoadTime(loader).build(model.thisClass().asSymbol(), builder -> {
// Everything the target had, first and unchanged — except a field whose final flag a
// @Shadow(mutable = true) asked to drop. A merged member that collided was already
// refused, so nothing here can be overwritten by what follows.
diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java
index b0e0ac3..246e70e 100644
--- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java
+++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java
@@ -4,8 +4,10 @@
import de.splatgames.aether.weaver.api.diagnostic.DiagnosticCode;
import de.splatgames.aether.weaver.api.diagnostic.WeaveException;
import de.splatgames.aether.weaver.api.spi.DiagnosticListener;
+import de.splatgames.aether.weaver.engine.internal.transform.FrameSupport;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.lang.classfile.ClassFile;
import java.util.List;
@@ -92,6 +94,37 @@ public Verifier(@NotNull final VerificationPolicy policy,
public byte[] check(@NotNull final String internalName,
final byte @NotNull [] original,
final byte @NotNull [] woven) {
+ return check(internalName, original, woven, null);
+ }
+
+ /**
+ * Checks a woven class against the type hierarchy the given loader describes.
+ *
+ * Identical to {@link #check(String, byte[], byte[])} except that {@link ClassFile#verify(
+ * byte[])} is run through a {@link FrameSupport#forLoadTime(ClassLoader)} context, so the
+ * assignability questions the verifier asks at frame merges are answered from the loader that
+ * defines the target rather than the system loader. Without it a woven class that merges the
+ * application's own types is reported as unverifiable purely because those types cannot be
+ * resolved, which is a false refusal rather than a real one.
+ *
+ * @param internalName the class's internal name, which the message names; must not be
+ * {@code null}
+ * @param original the class as it arrived, handed back when a refusal is not fatal; must
+ * not be {@code null}
+ * @param woven the class as weaving left it; must not be {@code null}
+ * @param loader the loader that defines the target, whose resources describe the
+ * hierarchy; {@code null} for the bootstrap loader
+ * @return {@code woven} when the policy checks nothing or nothing refuses it, and
+ * {@code original} when a refusal is not fatal
+ * @throws NullPointerException if {@code internalName}, {@code original} or {@code woven} is
+ * {@code null}
+ * @throws WeaveException if the policy is fatal and either check refuses the class
+ */
+ @NotNull
+ public byte[] check(@NotNull final String internalName,
+ final byte @NotNull [] original,
+ final byte @NotNull [] woven,
+ @Nullable final ClassLoader loader) {
Objects.requireNonNull(internalName, "internalName");
Objects.requireNonNull(original, "original");
Objects.requireNonNull(woven, "woven");
@@ -106,7 +139,10 @@ public byte[] check(@NotNull final String internalName,
if (!structural.isEmpty()) {
return refuse(structuralDiagnostic(internalName, structural), original);
}
- final List errors = ClassFile.of().verify(woven);
+ // Through the load-time context, so the verifier resolves the application's own types the
+ // same way stack-map generation did; a plain ClassFile.of() here uses the system loader and
+ // reports unresolvable application types as spurious verification errors.
+ final List errors = FrameSupport.forLoadTime(loader).verify(woven);
if (errors.isEmpty()) {
return woven;
}
diff --git a/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java b/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java
index f2c89b5..467d642 100644
--- a/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java
+++ b/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java
@@ -40,8 +40,8 @@ void header() {
final Weaver weaver = explaining(audit());
assertThat(weaver.explain().lines().findFirst().orElseThrow())
- .startsWith("Aether Weaver 0.1.0 — plan ")
- .hasSizeGreaterThan("Aether Weaver 0.1.0 — plan ".length());
+ .startsWith("Aether Weaver 0.1.1 — plan ")
+ .hasSizeGreaterThan("Aether Weaver 0.1.1 — plan ".length());
}
@Test
diff --git a/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java b/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java
index 75e6c7b..a58e69e 100644
--- a/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java
+++ b/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java
@@ -161,7 +161,7 @@ void onGreet() {
.as("this is the one driver whose report can be complete: the goal knows every "
+ "class it will ever be asked about, so by the time it prints, every "
+ "point has been resolved against real bytes")
- .anyMatch(line -> line.startsWith("Aether Weaver 0.1.0 — plan "))
+ .anyMatch(line -> line.startsWith("Aether Weaver 0.1.1 — plan "))
.anyMatch(line -> line.contains("Weaves (1):"))
.anyMatch(line -> line.contains("1 site @"))
.noneMatch(line -> line.contains("not woven yet"));