Skip to content
Merged
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Agents may run smaller, targeted checks such as `./mvnw test`, specific Maven te

## Coding Style & Naming Conventions

Follow `.editorconfig`: 2-space indentation, LF line endings, UTF-8, and final newlines. Prettier is the formatter of record, including Java via `prettier-plugin-java`; run it instead of hand-formatting. Checkstyle enforces import hygiene, visibility, declaration order, and standard Java naming. Match current naming patterns: `*Command`, `*Configuration`, `*Exception`, and package names in lowercase. Domain interfaces that represent ports should be named by business capability, such as `RuntimeExtensionInstaller`, `RuntimeDisplayReader`, or `RuntimeModeConfigurationRepository`; do not use the `Port` suffix for domain ports. Secondary implementations of domain ports should use mechanism, source, or context prefix plus capability, such as `FileSystemProjectsRepository`, `BootstrapRuntimeDisplayReader`, or `BootstrapRuntimeExtensionInstaller`. Reserve `Adapter` for technical wrappers that do not implement a domain port, such as adapters around APIs or frameworks. Prefer one public type per file. Do not add overloaded constructors, overloaded methods, factories, or defaulting shortcuts only to make tests shorter or more convenient. Tests should exercise the same intentional API shape used by production code. If a test needs deterministic or alternate behavior, introduce a meaningful production concept that is also connected to the runtime path, or test through the observable public behavior. In production code, order private helper methods immediately after their first use; when a helper is used from multiple places, place it after the earliest caller that introduces it. Avoid raw boolean parameters when they select a business behavior, mode, status, or strategy. Model that choice with a dedicated type, usually a small enum with named states and a predicate method, following patterns such as `BootstrapDebugMode`, `RuntimeProcessMode`, or `RuntimeExtensionReplacementStatus`. Avoid `is*` prefixes for boolean methods; prefer names that express the business predicate directly, such as `atLeast(...)`, `missingBootInfClasses(...)`, or `standardMode(...)`. Prefer `Optional` over direct `null` comparisons when modeling optional values. Do not use `@Autowired`; prefer a single explicit constructor and direct wiring. Do not use `var` in Java; prefer explicit types. When validating mandatory references in Java, do not use `Objects.requireNonNull`; use `Assert.notNull("fieldName", value)` from `src/main/java/com/seed4j/cli/shared/error/domain/Assert.java`.
Follow `.editorconfig`: 2-space indentation, LF line endings, UTF-8, and final newlines. Prettier is the formatter of record, including Java via `prettier-plugin-java`; run it instead of hand-formatting. Checkstyle enforces import hygiene, visibility, declaration order, and standard Java naming. Match current naming patterns: `*Command`, `*Configuration`, `*Exception`, and package names in lowercase. Domain interfaces that represent ports should be named by business capability, such as `RuntimeExtensionInstaller`, `RuntimeDisplayReader`, or `RuntimeModeConfigurationRepository`; do not use the `Port` suffix for domain ports. Secondary implementations of domain ports should use mechanism, source, or context prefix plus capability, such as `FileSystemProjectsRepository`, `BootstrapRuntimeDisplayReader`, or `BootstrapRuntimeExtensionInstaller`. Reserve `Adapter` for technical wrappers that do not implement a domain port, such as adapters around APIs or frameworks. Prefer one public type per file. Do not add overloaded constructors, overloaded methods, factories, or defaulting shortcuts only to make tests shorter or more convenient. Tests should exercise the same intentional API shape used by production code. If a test needs deterministic or alternate behavior, introduce a meaningful production concept that is also connected to the runtime path, or test through the observable public behavior. In production code, order private helper methods immediately after their first use; when a helper is used from multiple places, place it after the earliest caller that introduces it. Do not use mutable out-parameters, such as passing `List`, `Set`, or holder objects into helpers so they can append progress by side effect. Java passes object references by value; the issue is hidden mutation of shared accumulators. Prefer returning an explicit result or progress type and threading it through loops or recursion. Use private records for implementation-local progress/context types, and promote them only when they represent real domain concepts. When a helper needs to apply an ordered sequence of transformations to an immutable progress/result object, prefer a named fold over `forEach` with external state. A stream can be appropriate when each element is mapped to a clearly named `Function<Progress, Progress>` and composed with `Function::andThen` or an equally explicit fold. Use this only when the helper names make the data flow clearer than an explicit loop; do not use streams to hide mutation. Avoid raw boolean parameters when they select a business behavior, mode, status, or strategy. Model that choice with a dedicated type, usually a small enum with named states and a predicate method, following patterns such as `BootstrapDebugMode`, `RuntimeProcessMode`, or `RuntimeExtensionReplacementStatus`. Avoid `is*` prefixes for boolean methods; prefer names that express the business predicate directly, such as `atLeast(...)`, `missingBootInfClasses(...)`, or `standardMode(...)`. Prefer `Optional` over direct `null` comparisons when modeling optional values. Do not use `@Autowired`; prefer a single explicit constructor and direct wiring. Do not use `var` in Java; prefer explicit types. When validating mandatory references in Java, do not use `Objects.requireNonNull`; use `Assert.notNull("fieldName", value)` from `src/main/java/com/seed4j/cli/shared/error/domain/Assert.java`.

## Testing Guidelines

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ To inspect module parameters before applying a module, add `--plan`:
seed4j apply init --plan
```

The plan prints the resolved text values and shows whether each value came from the current CLI input, project history, or a module metadata default. If required values are missing, it prints a `Missing required parameters` section and still does not apply files, write history, or create commits.
The plan first prints a `Dependency plan`, then the resolved text values and whether each value came from the current CLI input, project history, or a module metadata default. Lines marked with `✓` are resolved, and lines marked with `○` are pending or missing. Dependency statuses show direct module dependencies as `already applied` or `pending`; feature dependencies show either `satisfied by <module>` or `pending choice` with visible candidate modules. If required values are missing, it prints a `Missing required parameters` section and still does not apply files, write history, or create commits.

To install Bash completion for the active runtime:

Expand Down
47 changes: 39 additions & 8 deletions documentation/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,32 +119,41 @@ seed4j apply init --plan

The plan is text-only and exits without generated files, history entries, or commits. Required parameters can come from
the current command or project history. When required parameters are still missing, the plan exits successfully and shows
which options to pass before applying the module.
which options to pass before applying the module. Lines marked with `✓` are resolved, and lines marked with `○` are
pending or missing.

```text
Plan for module: init
Project path: .

Dependency plan:

✓ No dependencies.

Resolved parameters:

endOfLine: lf
endOfLine: lf
Source: default
CLI option: --end-of-line

indentSize: 2
indentSize: 2
Source: default
CLI option: --indent-size

Missing required parameters:

projectName:
projectName:
CLI option: --project-name
Note: pass this option or apply a module that records it in project history.

baseName:
baseName:
CLI option: --base-name
Note: pass this option or apply a module that records it in project history.

○ nodePackageManager:
CLI option: --node-package-manager
Note: pass this option or apply a module that records it in project history.

No changes were applied.
```

Expand All @@ -164,21 +173,25 @@ seed4j apply maven-java --plan
Plan for module: maven-java
Project path: .

Dependency plan:

✓ module:init - already applied

Resolved parameters:

projectName: Seed4J Sample Application
projectName: Seed4J Sample Application
Source: project history
CLI option: --project-name
Note: already selected by project history; omit this option to keep it.

baseName: seed4jSampleApplication
baseName: seed4jSampleApplication
Source: project history
CLI option: --base-name
Note: already selected by project history; omit this option to keep it.

Missing required parameters:

packageName:
packageName:
CLI option: --package-name
Note: pass this option or apply a module that records it in project history.

Expand All @@ -191,6 +204,24 @@ Pass the missing option when applying the module:
seed4j apply maven-java --package-name com.mycompany.myapp
```

Dependency status labels mean:

- `✓ module:<slug> - already applied`: the module dependency is already recorded in project history
- `○ module:<slug> - pending`: the module dependency is not recorded in project history
- `✓ feature:<slug> - satisfied by <module>`: an applied module provides the required feature
- `○ feature:<slug> - pending choice: <candidate>, <candidate>`: no applied module provides the feature yet; choose one of
the sorted visible candidates

For example, after applying `maven-java`, planning `sonarqube-java-backend` can show one satisfied feature and one feature
that still needs a choice:

```text
Dependency plan:

○ feature:code-coverage-java - pending choice: jacoco, jacoco-with-min-coverage-check
✓ feature:java-build-tool - satisfied by maven-java
```

Plan source labels mean:

- `explicit CLI input`: the option was passed in the current command
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.seed4j.cli.command.infrastructure.primary;

import java.util.List;

record ApplyModuleDependencyPlan(List<ApplyModuleDependencyPlanLine> lines) {
boolean empty() {
return lines.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.seed4j.cli.command.infrastructure.primary;

record ApplyModuleDependencyPlanLine(String dependency, ApplyModuleDependencyStatus status) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package com.seed4j.cli.command.infrastructure.primary;

import com.seed4j.module.domain.landscape.Seed4JLandscapeDependency;
import com.seed4j.module.domain.resource.Seed4JModuleResource;
import com.seed4j.module.domain.resource.Seed4JModulesResources;
import com.seed4j.project.domain.ModuleSlug;
import com.seed4j.project.domain.history.ProjectAction;
import com.seed4j.project.domain.history.ProjectHistory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

class ApplyModuleDependencyPlanner {

ApplyModuleDependencyPlan plan(Seed4JModuleResource module, Seed4JModulesResources resources, ProjectHistory history) {
DependencyPlanningProgress progress = appendDependencies(
module,
DependencyPlanningContext.from(resources, history),
DependencyPlanningProgress.empty()
);

return new ApplyModuleDependencyPlan(progress.lines());
}

private static Set<String> appliedModules(ProjectHistory history) {
return history.actions().stream().map(ProjectAction::module).map(ModuleSlug::get).collect(Collectors.toUnmodifiableSet());
}

private static Comparator<Seed4JModuleResource> byModuleSlug() {
return Comparator.comparing(module -> module.slug().get());
}

private static DependencyPlanningProgress appendDependencies(
Seed4JModuleResource module,
DependencyPlanningContext context,
DependencyPlanningProgress progress
) {
String moduleSlug = module.slug().get();
if (progress.visitedModules().contains(moduleSlug)) {
return progress;
}

return module
.organization()
.dependencies()
.stream()
.sorted(byDependencyToken())
.map(dependency -> dependencyPlanningStep(dependency, context))
.reduce(Function.identity(), Function::andThen)
.apply(progress.withVisitedModule(moduleSlug));
}

private static Comparator<Seed4JLandscapeDependency> byDependencyToken() {
return Comparator.comparing(ApplyModuleDependencyPlanner::dependencyToken);
}

private static Function<DependencyPlanningProgress, DependencyPlanningProgress> dependencyPlanningStep(
Seed4JLandscapeDependency dependency,
DependencyPlanningContext context
) {
return progress -> appendDependency(dependency, context, progress);
}

private static DependencyPlanningProgress appendDependency(
Seed4JLandscapeDependency dependency,
DependencyPlanningContext context,
DependencyPlanningProgress progress
) {
return switch (dependency.type()) {
case MODULE -> appendModuleDependency(dependency, context, progress);
case FEATURE -> appendFeatureDependency(dependency, context, progress);
};
}

private static DependencyPlanningProgress appendModuleDependency(
Seed4JLandscapeDependency dependency,
DependencyPlanningContext context,
DependencyPlanningProgress progress
) {
String dependencySlug = dependency.slug().get();
DependencyPlanningProgress nextProgress = progress.withPlanLineIfMissing(
new ApplyModuleDependencyPlanLine(dependencyToken(dependency), moduleStatus(dependencySlug, context.appliedModules()))
);

return context
.module(dependencySlug)
.map(resource -> appendDependencies(resource, context, nextProgress))
.orElse(nextProgress);
}

private static ApplyModuleDependencyStatus moduleStatus(String dependencySlug, Set<String> appliedModules) {
if (appliedModules.contains(dependencySlug)) {
return ApplyModuleDependencyStatus.alreadyApplied();
}

return ApplyModuleDependencyStatus.pending();
}

private static DependencyPlanningProgress appendFeatureDependency(
Seed4JLandscapeDependency dependency,
DependencyPlanningContext context,
DependencyPlanningProgress progress
) {
return progress.withPlanLineIfMissing(
new ApplyModuleDependencyPlanLine(dependencyToken(dependency), featureStatus(dependency, context))
);
}

private static ApplyModuleDependencyStatus featureStatus(Seed4JLandscapeDependency dependency, DependencyPlanningContext context) {
List<String> candidates = featureCandidates(dependency.slug().get(), context.visibleModules());
return candidates
.stream()
.filter(context.appliedModules()::contains)
.findFirst()
.map(ApplyModuleDependencyStatus::satisfiedBy)
.orElseGet(() -> ApplyModuleDependencyStatus.pendingChoice(candidates));
}

private static List<String> featureCandidates(String featureSlug, List<Seed4JModuleResource> visibleModules) {
return visibleModules
.stream()
.filter(module ->
module
.organization()
.feature()
.map(feature -> feature.get().equals(featureSlug))
.orElse(false)
)
.map(module -> module.slug().get())
.sorted()
.toList();
}

private static String dependencyToken(Seed4JLandscapeDependency dependency) {
return dependency.type().name().toLowerCase() + ":" + dependency.slug().get();
}

private record DependencyPlanningContext(
Map<String, Seed4JModuleResource> modulesBySlug,
List<Seed4JModuleResource> visibleModules,
Set<String> appliedModules
) {
private DependencyPlanningContext {
modulesBySlug = Map.copyOf(modulesBySlug);
visibleModules = List.copyOf(visibleModules);
appliedModules = Set.copyOf(appliedModules);
}

private static DependencyPlanningContext from(Seed4JModulesResources resources, ProjectHistory history) {
List<Seed4JModuleResource> visibleModules = resources.stream().sorted(byModuleSlug()).toList();
return new DependencyPlanningContext(
visibleModules.stream().collect(Collectors.toMap(resource -> resource.slug().get(), Function.identity())),
visibleModules,
ApplyModuleDependencyPlanner.appliedModules(history)
);
}

private Optional<Seed4JModuleResource> module(String slug) {
return Optional.ofNullable(modulesBySlug.get(slug));
}
}

private record DependencyPlanningProgress(
List<ApplyModuleDependencyPlanLine> lines,
Set<String> plannedDependencies,
Set<String> visitedModules
) {
private DependencyPlanningProgress {
lines = List.copyOf(lines);
plannedDependencies = Collections.unmodifiableSet(new LinkedHashSet<>(plannedDependencies));
visitedModules = Collections.unmodifiableSet(new LinkedHashSet<>(visitedModules));
}

private static DependencyPlanningProgress empty() {
return new DependencyPlanningProgress(List.of(), Set.of(), Set.of());
}

private DependencyPlanningProgress withVisitedModule(String moduleSlug) {
Set<String> nextVisitedModules = new LinkedHashSet<>(visitedModules);
nextVisitedModules.add(moduleSlug);
return new DependencyPlanningProgress(lines, plannedDependencies, nextVisitedModules);
}

private DependencyPlanningProgress withPlanLineIfMissing(ApplyModuleDependencyPlanLine line) {
if (plannedDependencies.contains(line.dependency())) {
return this;
}

List<ApplyModuleDependencyPlanLine> nextLines = new ArrayList<>(lines);
Set<String> nextPlannedDependencies = new LinkedHashSet<>(plannedDependencies);
nextLines.add(line);
nextPlannedDependencies.add(line.dependency());
return new DependencyPlanningProgress(nextLines, nextPlannedDependencies, visitedModules);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.seed4j.cli.command.infrastructure.primary;

import com.seed4j.cli.shared.error.domain.Assert;
import java.util.List;

record ApplyModuleDependencyStatus(StatusKind kind, String moduleSlug, List<String> candidates) {
ApplyModuleDependencyStatus {
candidates = List.copyOf(candidates);
}

static ApplyModuleDependencyStatus alreadyApplied() {
return new ApplyModuleDependencyStatus(StatusKind.ALREADY_APPLIED, "", List.of());
}

static ApplyModuleDependencyStatus pending() {
return new ApplyModuleDependencyStatus(StatusKind.PENDING, "", List.of());
}

static ApplyModuleDependencyStatus satisfiedBy(String moduleSlug) {
return new ApplyModuleDependencyStatus(StatusKind.SATISFIED_BY, moduleSlug, List.of());
}

static ApplyModuleDependencyStatus pendingChoice(List<String> candidates) {
Assert.notEmpty("candidates", candidates);

return new ApplyModuleDependencyStatus(StatusKind.PENDING_CHOICE, "", candidates);
}

String displayLabel() {
return switch (kind) {
case ALREADY_APPLIED -> "already applied";
case PENDING -> "pending";
case SATISFIED_BY -> "satisfied by " + moduleSlug;
case PENDING_CHOICE -> "pending choice: " + String.join(", ", candidates);
};
}

ApplyModulePlanItemMarker planMarker() {
return switch (kind) {
case ALREADY_APPLIED, SATISFIED_BY -> ApplyModulePlanItemMarker.RESOLVED;
case PENDING, PENDING_CHOICE -> ApplyModulePlanItemMarker.PENDING;
};
}

private enum StatusKind {
ALREADY_APPLIED,
PENDING,
SATISFIED_BY,
PENDING_CHOICE,
}
}
Loading