diff --git a/docs/packer/pull-requests/PR-21-point-in-memory-snapshot-updates-after-write-commit.md b/docs/packer/pull-requests/PR-21-point-in-memory-snapshot-updates-after-write-commit.md new file mode 100644 index 00000000..6005531e --- /dev/null +++ b/docs/packer/pull-requests/PR-21-point-in-memory-snapshot-updates-after-write-commit.md @@ -0,0 +1,77 @@ +# PR-21 Point In-Memory Snapshot Updates After Write Commit + +Domain Owner: `docs/packer` +Cross-Domain Impact: `docs/studio` + +## Briefing + +The current write path still rebuilds the whole project runtime snapshot after a successful point write such as `REGISTER`. + +That is too expensive for the runtime model we want. + +Studio refresh on top of an already-updated in-memory snapshot is acceptable. Full filesystem rescan inside the packer after every point write is not. + +This PR replaces the normal post-write `full refresh` path with point snapshot updates in memory, while keeping full reload available only as an explicit recovery fallback. + +## Objective + +Remove whole-project runtime rescan from the normal point write path and replace it with point in-memory snapshot updates after durable commit. + +## Dependencies + +- [`./PR-14-project-runtime-core-snapshot-model-and-lifecycle.md`](./PR-14-project-runtime-core-snapshot-model-and-lifecycle.md) +- [`./PR-15-snapshot-backed-asset-query-services.md`](./PR-15-snapshot-backed-asset-query-services.md) +- [`./PR-16-write-lane-command-completion-and-used-write-services.md`](./PR-16-write-lane-command-completion-and-used-write-services.md) +- [`./PR-20-asset-action-capabilities-and-register-first-delivery.md`](./PR-20-asset-action-capabilities-and-register-first-delivery.md) + +## Scope + +- add explicit point snapshot update support to the runtime registry +- keep the write lane as the only owner of point runtime mutation after durable commit +- replace post-write `runtimeRegistry.refresh(project)` in the active write path with point snapshot patching +- deliver the first point patch for `REGISTER` +- keep Studio refresh behavior unchanged + +## Non-Goals + +- no background divergence detection +- no reconcile loop +- no change to Studio refresh semantics +- no broad event model redesign +- no speculative patch implementation for unused write actions + +## Execution Method + +1. Extend the runtime registry so it can update a loaded project snapshot by applying a point patch function inside the packer-owned write flow. +2. Keep full snapshot reload available as recovery fallback, but do not use it in the normal successful point write path. +3. Model a point patch for `REGISTER` that: + - appends the new registry entry to the in-memory registry view + - updates the matching runtime asset from unregistered to registered + - preserves the rest of the snapshot untouched +4. Apply the patch only after the durable disk commit succeeds. +5. Keep read services unchanged from the Studio point of view; they should continue reading from the runtime snapshot. +6. Keep Studio free to refresh list/details after write completion, because those reads now hit the already-updated in-memory snapshot. +7. Add regression coverage proving that `REGISTER` no longer depends on whole-project rescan to become visible to subsequent reads. + +## Acceptance Criteria + +- successful point writes no longer trigger whole-project runtime rescan in the normal path +- the runtime registry supports point in-memory snapshot updates after durable commit +- `REGISTER` updates the loaded project snapshot in place +- subsequent `listAssets`, `getAssetDetails`, and `getAssetActions` read the updated state from memory +- Studio refresh after `REGISTER` remains valid and does not require frontend-local state patching +- full reload remains available only as fallback or explicit recovery path + +## Validation + +- packer tests for point snapshot patching on `REGISTER` +- packer tests proving read-after-write coherence without full runtime rebuild +- regression tests for fallback safety when point patching cannot be applied +- Studio smoke validation confirming existing refresh behavior still works on top of the updated runtime snapshot + +## Affected Artifacts + +- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/**` +- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/**` +- `prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/**` +- `prometeu-studio/src/main/java/p/studio/**` diff --git a/docs/packer/pull-requests/README.md b/docs/packer/pull-requests/README.md index c61dc740..c708f93d 100644 --- a/docs/packer/pull-requests/README.md +++ b/docs/packer/pull-requests/README.md @@ -80,6 +80,7 @@ The current production track for the standalone `prometeu-packer` project is: 18. [`PR-18-legacy-service-retirement-and-regression-hardening.md`](./PR-18-legacy-service-retirement-and-regression-hardening.md) 19. [`PR-19-api-surface-audit-model-separation-and-public-read-dtos.md`](./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md) 20. [`PR-20-asset-action-capabilities-and-register-first-delivery.md`](./PR-20-asset-action-capabilities-and-register-first-delivery.md) +21. [`PR-21-point-in-memory-snapshot-updates-after-write-commit.md`](./PR-21-point-in-memory-snapshot-updates-after-write-commit.md) Current wave discipline from `PR-11` onward: @@ -90,4 +91,4 @@ Current wave discipline from `PR-11` onward: Recommended dependency chain: -`PR-01 -> PR-02 -> PR-03 -> PR-04 -> PR-05 -> PR-06 -> PR-07 -> PR-08 -> PR-09 -> PR-10 -> PR-11 -> PR-12 -> PR-13 -> PR-14 -> PR-15 -> PR-16 -> PR-17 -> PR-18 -> PR-19 -> PR-20` +`PR-01 -> PR-02 -> PR-03 -> PR-04 -> PR-05 -> PR-06 -> PR-07 -> PR-08 -> PR-09 -> PR-10 -> PR-11 -> PR-12 -> PR-13 -> PR-14 -> PR-15 -> PR-16 -> PR-17 -> PR-18 -> PR-19 -> PR-20 -> PR-21` diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java index e8c83d93..72a49178 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java @@ -32,11 +32,13 @@ public final class Packer implements Closeable { runtimeRegistry, assetReferenceResolver, workspaceFoundation.lookup()); + final PackerRuntimePatchService runtimePatchService = new PackerRuntimePatchService(declarationParser); final PackerProjectWriteCoordinator writeCoordinator = new PackerProjectWriteCoordinator(); return new Packer(new FileSystemPackerWorkspaceService( workspaceFoundation, assetDetailsService, assetActionReadService, + runtimePatchService, runtimeRegistry, writeCoordinator, resolvedEventSink), runtimeRegistry, writeCoordinator); diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java index 83658c4b..4d816471 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java @@ -47,6 +47,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe private final PackerWorkspaceFoundation workspaceFoundation; private final PackerAssetDetailsService detailsService; private final PackerAssetActionReadService actionReadService; + private final PackerRuntimePatchService runtimePatchService; private final PackerRuntimeRegistry runtimeRegistry; private final PackerProjectWriteCoordinator writeCoordinator; private final PackerEventSink eventSink; @@ -55,12 +56,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe PackerWorkspaceFoundation workspaceFoundation, PackerAssetDetailsService detailsService, PackerAssetActionReadService actionReadService, + PackerRuntimePatchService runtimePatchService, PackerRuntimeRegistry runtimeRegistry, PackerProjectWriteCoordinator writeCoordinator, PackerEventSink eventSink) { this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation"); this.detailsService = Objects.requireNonNull(detailsService, "detailsService"); this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService"); + this.runtimePatchService = Objects.requireNonNull(runtimePatchService, "runtimePatchService"); this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry"); this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator"); this.eventSink = Objects.requireNonNull(eventSink, "eventSink"); @@ -218,7 +221,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe writeManifest(manifestPath, request, entry.assetUuid()); final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry); workspaceFoundation.saveRegistry(project, updated); - runtimeRegistry.refresh(project); + runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset( + snapshot, + generation, + updated, + entry, + assetRoot, + manifestPath)); final CreateAssetResult result = new CreateAssetResult( PackerOperationStatus.SUCCESS, "Asset created: " + relativeAssetRoot, @@ -262,7 +271,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe declaration.assetUuid()); final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry); workspaceFoundation.saveRegistry(project, updated); - runtimeRegistry.refresh(project); + runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterRegisterAsset( + currentSnapshot, + generation, + updated, + entry, + assetRoot)); final RegisterAssetResult result = new RegisterAssetResult( PackerOperationStatus.SUCCESS, "Asset registered: " + relativeAssetRoot, diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerProjectRuntime.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerProjectRuntime.java index ac237a5f..3c21ed66 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerProjectRuntime.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerProjectRuntime.java @@ -6,6 +6,7 @@ import p.packer.models.PackerRuntimeSnapshot; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.UnaryOperator; public final class PackerProjectRuntime { private final PackerProjectContext project; @@ -31,6 +32,13 @@ public final class PackerProjectRuntime { snapshot.set(Objects.requireNonNull(nextSnapshot, "nextSnapshot")); } + public PackerRuntimeSnapshot updateSnapshot(UnaryOperator updater) { + ensureActive(); + return snapshot.updateAndGet(current -> Objects.requireNonNull( + Objects.requireNonNull(updater, "updater").apply(current), + "updatedSnapshot")); + } + public boolean disposed() { return disposed.get(); } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeLoader.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeLoader.java index c0264b96..430943c2 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeLoader.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeLoader.java @@ -19,7 +19,7 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; -public final class PackerRuntimeLoader { +public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader { private final PackerWorkspaceFoundation workspaceFoundation; private final PackerAssetDeclarationParser parser; @@ -30,6 +30,7 @@ public final class PackerRuntimeLoader { this.parser = Objects.requireNonNull(parser, "parser"); } + @Override public PackerRuntimeSnapshot load(PackerProjectContext project, long generation) { final PackerProjectContext safeProject = Objects.requireNonNull(project, "project"); workspaceFoundation.initWorkspace(new InitWorkspaceRequest(safeProject)); diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimePatchService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimePatchService.java new file mode 100644 index 00000000..586d9f7c --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimePatchService.java @@ -0,0 +1,68 @@ +package p.packer.services; + +import p.packer.models.PackerAssetDeclarationParseResult; +import p.packer.models.PackerRegistryEntry; +import p.packer.models.PackerRegistryState; +import p.packer.models.PackerRuntimeAsset; +import p.packer.models.PackerRuntimeSnapshot; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public final class PackerRuntimePatchService { + private final PackerAssetDeclarationParser declarationParser; + + public PackerRuntimePatchService(PackerAssetDeclarationParser declarationParser) { + this.declarationParser = Objects.requireNonNull(declarationParser, "declarationParser"); + } + + public PackerRuntimeSnapshot afterCreateAsset( + PackerRuntimeSnapshot snapshot, + long generation, + PackerRegistryState updatedRegistry, + PackerRegistryEntry entry, + Path assetRoot, + Path manifestPath) { + final PackerAssetDeclarationParseResult parsed = declarationParser.parse(manifestPath); + final List updatedAssets = new ArrayList<>(snapshot.assets().stream() + .filter(candidate -> !candidate.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) + .toList()); + updatedAssets.add(new PackerRuntimeAsset( + assetRoot, + manifestPath, + Optional.of(entry), + parsed)); + updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)); + return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); + } + + public PackerRuntimeSnapshot afterRegisterAsset( + PackerRuntimeSnapshot snapshot, + long generation, + PackerRegistryState updatedRegistry, + PackerRegistryEntry entry, + Path assetRoot) { + final List updatedAssets = new ArrayList<>(); + boolean patched = false; + for (PackerRuntimeAsset asset : snapshot.assets()) { + if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) { + updatedAssets.add(new PackerRuntimeAsset( + asset.assetRoot(), + asset.manifestPath(), + Optional.of(entry), + asset.parsedDeclaration())); + patched = true; + } else { + updatedAssets.add(asset); + } + } + if (!patched) { + throw new IllegalStateException("Unable to patch runtime snapshot for unregistered asset: " + assetRoot); + } + return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeRegistry.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeRegistry.java index 763fe1ff..671d6b81 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeRegistry.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeRegistry.java @@ -1,8 +1,10 @@ package p.packer.services; import p.packer.messages.PackerProjectContext; +import p.packer.models.PackerRuntimeSnapshot; import java.nio.file.Path; +import java.util.function.BiFunction; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -10,11 +12,11 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; public final class PackerRuntimeRegistry { - private final PackerRuntimeLoader loader; + private final PackerRuntimeSnapshotLoader loader; private final ConcurrentMap runtimes = new ConcurrentHashMap<>(); private final AtomicLong nextGeneration = new AtomicLong(1L); - public PackerRuntimeRegistry(PackerRuntimeLoader loader) { + public PackerRuntimeRegistry(PackerRuntimeSnapshotLoader loader) { this.loader = Objects.requireNonNull(loader, "loader"); } @@ -46,6 +48,27 @@ public final class PackerRuntimeRegistry { }); } + public PackerProjectRuntime update( + PackerProjectContext project, + BiFunction updater) { + final PackerProjectContext safeProject = Objects.requireNonNull(project, "project"); + final BiFunction safeUpdater = + Objects.requireNonNull(updater, "updater"); + final ProjectKey key = ProjectKey.from(safeProject); + return runtimes.compute(key, (ignored, current) -> { + if (current == null || current.disposed()) { + return new PackerProjectRuntime(safeProject, loader.load(safeProject, nextGeneration.getAndIncrement())); + } + try { + final long generation = nextGeneration.getAndIncrement(); + current.updateSnapshot(snapshot -> safeUpdater.apply(snapshot, generation)); + } catch (RuntimeException exception) { + current.replaceSnapshot(loader.load(safeProject, nextGeneration.getAndIncrement())); + } + return current; + }); + } + public void dispose(PackerProjectContext project) { final PackerProjectRuntime removed = runtimes.remove(ProjectKey.from(Objects.requireNonNull(project, "project"))); if (removed != null) { diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeSnapshotLoader.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeSnapshotLoader.java new file mode 100644 index 00000000..65b2bb45 --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimeSnapshotLoader.java @@ -0,0 +1,9 @@ +package p.packer.services; + +import p.packer.messages.PackerProjectContext; +import p.packer.models.PackerRuntimeSnapshot; + +@FunctionalInterface +public interface PackerRuntimeSnapshotLoader { + PackerRuntimeSnapshot load(PackerProjectContext project, long generation); +} diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java index e3d3ba9e..f4b6e6a1 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java @@ -147,6 +147,32 @@ final class FileSystemPackerWorkspaceServiceTest { assertTrue(detailsResult.diagnostics().isEmpty()); } + @Test + void createAssetPatchesLoadedSnapshotWithoutWholeProjectReload() throws Exception { + final Path projectRoot = tempDir.resolve("created-no-reload"); + final CountingLoader loader = countingLoader(); + final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader); + + service.listAssets(new ListAssetsRequest(project(projectRoot))); + assertEquals(1, loader.loadCount()); + + final CreateAssetResult createResult = service.createAsset(new CreateAssetRequest( + project(projectRoot), + "ui/new-atlas", + "new_atlas", + AssetFamilyCatalog.IMAGE_BANK, + OutputFormatCatalog.TILES_INDEXED_V1, + OutputCodecCatalog.NONE, + true)); + + assertEquals(PackerOperationStatus.SUCCESS, createResult.status()); + assertEquals(1, loader.loadCount()); + + final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), createResult.assetReference())); + assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status()); + assertEquals(1, loader.loadCount()); + } + @Test void exposesRegisterActionForValidUnregisteredAsset() throws Exception { final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan-actions")); @@ -201,6 +227,29 @@ final class FileSystemPackerWorkspaceServiceTest { assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED)); } + @Test + void registerAssetPatchesLoadedSnapshotWithoutWholeProjectReload() throws Exception { + final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("register-no-reload")); + final CountingLoader loader = countingLoader(); + final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader); + + service.listAssets(new ListAssetsRequest(project(projectRoot))); + assertEquals(1, loader.loadCount()); + + final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_sounds"))); + + assertEquals(PackerOperationStatus.SUCCESS, registerResult.status()); + assertEquals(1, loader.loadCount()); + + final var actionsResult = service.getAssetActions(new GetAssetActionsRequest( + project(projectRoot), + registerResult.assetReference())); + assertTrue(actionsResult.actions().isEmpty()); + assertEquals(1, loader.loadCount()); + } + @Test void blocksRegisterActionForInvalidDeclaration() throws Exception { final Path projectRoot = copyFixture("workspaces/invalid-missing-fields", tempDir.resolve("invalid-actions")); @@ -341,27 +390,59 @@ final class FileSystemPackerWorkspaceServiceTest { } private FileSystemPackerWorkspaceService service() { - return service(ignored -> { - }); + return service(ignored -> { }); } private FileSystemPackerWorkspaceService service(p.packer.events.PackerEventSink eventSink) { + return service(eventSink, countingLoader()); + } + + private FileSystemPackerWorkspaceService service( + p.packer.events.PackerEventSink eventSink, + PackerRuntimeSnapshotLoader loader) { final var foundation = new p.packer.services.PackerWorkspaceFoundation(); final var parser = new p.packer.services.PackerAssetDeclarationParser(); - final var runtimeRegistry = new p.packer.services.PackerRuntimeRegistry(new p.packer.services.PackerRuntimeLoader(foundation, parser)); + final var runtimeRegistry = new p.packer.services.PackerRuntimeRegistry(loader); final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup()); final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver); final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup()); + final var runtimePatchService = new p.packer.services.PackerRuntimePatchService(parser); final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator(); return new FileSystemPackerWorkspaceService( foundation, detailsService, actionReadService, + runtimePatchService, runtimeRegistry, writeCoordinator, eventSink); } + private CountingLoader countingLoader() { + final var foundation = new p.packer.services.PackerWorkspaceFoundation(); + final var parser = new p.packer.services.PackerAssetDeclarationParser(); + return new CountingLoader(new p.packer.services.PackerRuntimeLoader(foundation, parser)); + } + + private static final class CountingLoader implements PackerRuntimeSnapshotLoader { + private final PackerRuntimeSnapshotLoader delegate; + private int loadCount; + + private CountingLoader(PackerRuntimeSnapshotLoader delegate) { + this.delegate = delegate; + } + + @Override + public synchronized p.packer.models.PackerRuntimeSnapshot load(PackerProjectContext project, long generation) { + loadCount += 1; + return delegate.load(project, generation); + } + + private synchronized int loadCount() { + return loadCount; + } + } + private Path copyFixture(String relativePath, Path targetRoot) throws Exception { final Path sourceRoot = PackerFixtureLocator.fixtureRoot(relativePath); try (var stream = Files.walk(sourceRoot)) {