Skip to content
This repository was archived by the owner on Feb 4, 2026. It is now read-only.

Release 0.5.0: Simplified Slice Architecture#20

Merged
siy merged 27 commits intomainfrom
release-0.5.0
Jan 26, 2026
Merged

Release 0.5.0: Simplified Slice Architecture#20
siy merged 27 commits intomainfrom
release-0.5.0

Conversation

@siy
Copy link
Owner

@siy siy commented Jan 23, 2026

Summary

Major refactor of slice architecture plus JBCT compliance improvements and bug fixes.

Breaking Changes

Simplified Slice Architecture:

  • Removed separate API interface generation (no more .api package)
  • Single JAR per slice (removed API JAR)
  • All dependencies go through SliceInvokerFacade proxy
  • Consumers depend on @Slice interface directly

Major Features

Slice Architecture Simplification

  • Removed ApiInterfaceGenerator and API package generation
  • Updated SliceProcessor, SliceModel, ManifestGenerator
  • All dependencies generate proxies via invoker (no internal/external distinction)
  • Single JAR packaging in Maven plugin
  • Removed JBCT-SLICE-01 lint rule
  • Updated all documentation and RFCs

Annotation Processing

  • Added @Aspect and @Key annotation processing for cache-wrapped methods
  • Method name validation per RFC-0001 (^[a-z][a-zA-Z0-9]+$)
  • Factory method name follows RFC-0001 convention

Security & Validation

  • Dependency scope validation for Aether runtime libraries
  • jbct:verify-slice fails if runtime deps not provided scope
  • Path traversal protection
  • URL validation for GitHub downloads

JBCT Compliance

  • 71+ JBCT pattern compliance fixes across codebase
  • Nullable parameter rule (JBCT-NULL-02)
  • Enhanced lambda complexity detection
  • FQCN rule enhancement (JBCT-STY-03)

Bug Fixes

  • Fixed slice artifact naming in manifests
  • Corrected MethodHandle API usage
  • Fixed route config optional handling
  • Filtered framework transitives from dependency files
  • Fixed Result.async() usage for pragmatica-lite 0.10.0

Documentation

  • Updated 7 local docs files
  • Updated 4 RFCs (RFC-0001, RFC-0002, RFC-0004, RFC-0007)
  • Comprehensive changelog

Migration Guide

For slice projects:

  1. Remove API JAR dependencies - depend on impl JAR directly
  2. Update imports - use @Slice interface, not .api. package
  3. All runtime deps must use provided scope
  4. Single JAR per slice in packaging

For slice consumers:

  • No changes needed if already depending on impl JAR
  • If depending on API JAR, switch to impl JAR

Test Results

  • jbct-core: 222 tests ✅
  • slice-processor: 82 tests ✅
  • All modules compile and verify ✅

Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • GitHub-backed AI tools fetcher, secure URL/path validation, aspect-aware dependency wiring, and unified single “slice” JAR with proxy-based runtime wiring.
  • Bug Fixes

    • Fixed artifact naming, dependency scope/filtering, path escaping, packaging/deployment and installer download validation.
  • Documentation

    • Docs updated across guides and README to reflect single-JAR packaging, wiring changes, and v0.5.0 versioning.
  • Refactor

    • Stronger immutability, Option-based optionals, improved logging, and clearer generation/validation flows.
  • Tests

    • Expanded formatter, linter, proxy/packaging, and integration tests.

✏️ Tip: You can customize this high-level summary in your review settings.

siy and others added 23 commits January 15, 2026 21:40
- Security: add PathValidation, UrlValidation, SecurityError types
- Null policy: replace nullable with Option across codebase
- Factory methods: add missing factories for records
- Composition: extract complex lambdas to named methods
- Fork-Join: use Result.all() for parallel operations
- Thread safety: document non-thread-safe classes, eager init
- Immutability: defensive copies in record constructors
- Shared utilities: GitHubContentFetcher reduces duplication
- Add AspectModel and KeyExtractorInfo model classes
- Extend MethodModel to extract aspect annotations from methods
- Update FactoryClassGenerator to generate wrapper records with caching
- Add SliceRuntime/AspectFactory integration for cache creation
- Add infra-aspect dependency (aether 0.8.0)
- Add 5 tests for aspect annotation processing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- KeyExtractorInfo: Result-returning factories, validation
- DependencyModel: empty string guard, acronym handling
- MethodModel: extract lambdas to methods, stream-based processing
- RouteConfigLoader: extract methods, use System.Logger
- RouteSourceGenerator: escape paths and query params
- DependencyVersionResolver: handle IllegalArgumentException
- CstFullyQualifiedNameRule: flag java.lang.* FQCNs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Format all modules with jbct:format
- Update CHANGELOG with @Aspect/@key processing, FQCN rule enhancement
- Update release date to 2026-01-18

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- ManifestGenerator now writes slice artifact as groupId:artifactId-sliceName
- FactoryClassGenerator adds .async() for Result->Promise conversion
- Updated test stubs to match real SliceInvokerFacade interface
- Added test for slice artifact naming convention

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…e libs

- VerifySliceMojo now checks pragmatica-lite and aether dependencies
- Errors if runtime libs are not marked as 'provided' scope
- Updated slice template to use provided scope for all runtime deps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- isAetherRuntime now filters all org.pragmatica-lite and org.pragmatica-lite.aether
- Only include direct dependencies in dependencies file sections
- Prevents Netty/Jackson transitives from http-routing-adapter in slice deps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- toApiArtifactInfo reads api.artifact from META-INF/slice-api.properties
- toSliceArtifactInfo reads slice.artifact from manifest
- Fixes incorrect artifact names like inventory-api instead of inventory-inventory-service-api

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove ApiInterfaceGenerator and api package generation
- All dependencies now go through SliceInvokerFacade proxy
- Single JAR per slice (no separate API JAR)
- Remove JBCT-SLICE-01 lint rule (no longer applicable)
- Add slice dependency scope validation to VerifySliceMojo
- Update all documentation and RFCs

BREAKING CHANGE: Slices no longer generate separate API interfaces.
Consumers should depend on the @slice interface directly.
@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Consolidates slice packaging into a single Slice JAR, replaces API-JAR flows with generated factory+manifest, routes all dependencies via MethodHandle-based proxies through SliceInvokerFacade, adds aspect/key support and cache key extraction, and introduces security/fetch utilities (SecurityError, PathValidation, UrlValidation, GitHubContentFetcher).

Changes

Cohort / File(s) Summary
Release & Docs
CHANGELOG.md, README.md, CLAUDE.md, docs/slice/*, docs/slice/**
Bump to 0.5.0, remove Slice API/JAR split from docs, document single Slice JAR and invoker-proxy wiring.
Build & BOM
pom.xml, **/pom.xml, jbct-maven-plugin/pom.xml, slice-processor/pom.xml, slice-processor-tests/pom.xml
Version bumps to 0.5.0, add infra-aspect, update pragmatica-lite/aether versions, add tinylog test deps.
Security & Fetching Utilities
jbct-core/src/main/java/org/pragmatica/jbct/shared/\SecurityError.java`, PathValidation.java, UrlValidation.java, GitHubContentFetcher.java``
New sealed SecurityError types; path/URL validation helpers; GitHubContentFetcher for commit/tree/file discovery and downloads.
Slice processor core
slice-processor/src/main/java/org/pragmatica/jbct/slice/SliceProcessor.java
Remove ApiInterfaceGenerator, add Aspect/Key annotation support, switch to concurrent Result.all artifact generation, adapt route loading.
Factory & Proxy generation (large)
slice-processor/src/main/java/.../FactoryClassGenerator.java
Major rewrite: unified dependency handling, introduces MethodHandle/HandleInfo/ProxyMethodInfo, generates local proxy records and aspect wrappers routing through SliceInvokerFacade.
Manifest & artifact model
slice-processor/.../ManifestGenerator.java, jbct-core/.../SliceManifest.java, slice-processor/.../DependencyVersionResolver.java
Emit slice.artifact (impl-centric), remove apiClasses/apiArtifactId, preload dependency mappings; adjust manifest format and defensive immutability.
Slice model & aspects
slice-processor/src/main/java/org/pragmatica/jbct/slice/model/{AspectModel,KeyExtractorInfo,MethodModel,SliceModel,DependencyModel}.java
New AspectModel and KeyExtractorInfo; MethodModel gains aspects and requires ProcessingEnvironment; updated acronym handling; defensive copies added.
Routing & route generation
slice-processor/src/main/java/org/pragmatica/jbct/slice/routing/{RouteSourceGenerator,RouteConfig,RouteConfigLoader,ErrorPatternConfig,ErrorTypeMapping}.java
Add path escaping, Option-based lookups/merges, immutability for route maps, rename ErrorTypeMapping factories — inspect escaped literals and Option usage.
Lint rules & parser/formatter
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/*, jbct-core/src/main/java/org/pragmatica/jbct/parser/{Java25Parser,CstNodes}.java, jbct-core/src/main/java/org/pragmatica/jbct/format/cst/*
Add CstNullableParameterRule; remove CstSliceApiUsageRule; parser factory renames and CstFormatter no-arg factory; many rule tweaks and formatting-only edits.
Maven plugin: packaging & verification
jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/{PackageSlicesMojo,InstallSlicesMojo,DeploySlicesMojo,GenerateBlueprintMojo,VerifySliceMojo}
Remove API artifact install/deploy, consolidate impl-only JAR/POM, tighten dependency classification (direct vs transitive), add VerifySliceMojo scope validations (provided).
Init / Update / Upgrade & tooling
jbct-core/src/main/java/org/pragmatica/jbct/{init,update,upgrade}/*
AiToolsInstaller/Updater and AiToolsUpdater use GitHubContentFetcher/PathValidation; GitHubVersionResolver becomes Result-based with defaults; JarInstaller uses UrlValidation and improved logging.
CLI & internal refactors
jbct-cli/src/main/java/org/pragmatica/jbct/cli/{FormatCommand,InitCommand,UpdateCommand,UpgradeCommand}.java
Extract private helpers, switch from fold to onSuccess/onFailure flows; behavior preserved, improved readability/testability.
Tests & CI
jbct-core/src/test/**, slice-processor/src/test/**, .github/workflows/ci.yml
Tests updated for formatting/idempotency and new MethodHandle/aspect semantics; slice-processor tests expanded; CI excludes slice-processor-tests module from verify.

Sequence Diagram(s)

sequenceDiagram
    participant Processor as SliceProcessor
    participant Model as SliceModel
    participant Generator as FactoryClassGenerator
    participant Facade as SliceInvokerFacade
    participant Runtime as SliceRuntime
    participant Repo as ArtifactRepo

    Processor->>Model: extractMethods(element, env)
    Processor->>Generator: generate(sliceModel)
    Generator->>Facade: methodHandle(artifact, method, requestType, responseType)
    Facade->>Runtime: request runtime MethodHandle/adapter
    Runtime-->>Facade: Result<MethodHandle>
    Facade-->>Generator: Result<MethodHandle>
    Generator->>Generator: generate local proxy records + aspect wrappers
    Generator->>Repo: emit factory class & slice manifest
    Repo-->>Processor: artifacts written
Loading
sequenceDiagram
    participant Maven as jbct-maven-plugin
    participant Package as PackageSlicesMojo
    participant Resolver as DependencyVersionResolver
    participant Manifest as SliceManifest
    participant Verifier as VerifySliceMojo

    Maven->>Package: execute()
    Package->>Resolver: read slice manifests from dependencies
    Resolver-->>Package: artifact coordinates
    Package->>Manifest: build slice.artifact and impl classes
    Package->>Verifier: checkRuntimeDependencyScopes(deps)
    Verifier->>Verifier: validate 'provided' scope for runtime & slice deps
    Verifier-->>Package: ValidationResult
    Package->>Maven: create impl JAR and POM (no API JAR)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nibbling cheer,

One JAR now holds the slice so near.
Proxies spring and MethodHandles sing,
Keys and caches dance in a ring.
Fetch, validate, bundle — hip, hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Release 0.5.0: Simplified Slice Architecture' clearly and concisely summarizes the primary objective of the changeset: a major version release with a simplified architecture for slice processing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstAlwaysSuccessResultRule.java (1)

49-51: Boolean expression continuation lacks proper indentation.

The continuation lines of the || expression are not indented, making the code difficult to read and inconsistent with formatting standards.

🔧 Proposed fix
         boolean hasFailure = methodText.contains("Result.failure(") ||
-        methodText.contains(".result()") || // cause.result()
-        methodText.contains("failure");
+                             methodText.contains(".result()") || // cause.result()
+                             methodText.contains("failure");
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstReturnKindRule.java (1)

59-74: Handle fully qualified forbidden types (e.g., java.util.Optional).

typeText comparisons won’t match java.util.Optional<T> or java.util.concurrent.CompletableFuture<T>, so forbidden types can slip through. Consider normalizing to a simple type name before the checks.

🔧 Suggested normalization (minimal)
-        var typeText = text(type, source).trim();
+        var typeText = text(type, source).trim();
+        var normalizedType = typeText.replaceFirst("^.*\\.", "");
         var methodName = childByRule(method, RuleId.Identifier.class).map(id -> text(id, source))
                                     .or("(unknown)");
         // Check for void
-        if (typeText.equals("void")) {
+        if (normalizedType.equals("void")) {
             return Stream.of(createVoidDiagnostic(method, methodName, ctx));
         }
         // Check for forbidden types
         for (var forbidden : FORBIDDEN_TYPES) {
-            if (typeText.startsWith(forbidden + "<") || typeText.equals(forbidden)) {
+            if (normalizedType.startsWith(forbidden + "<") || normalizedType.equals(forbidden)) {
                 return Stream.of(createForbiddenTypeDiagnostic(method, methodName, typeText, ctx));
             }
         }
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstZoneThreeVerbsRule.java (2)

147-155: Avoid suggesting a Zone‑2 verb as a Zone‑3 alternative.

The suggestion "check" is itself in ZONE_2_VERBS, so the diagnostic points to a verb the rule flags. Let these fall through to the Zone‑3 default (or map to explicit Zone‑3 verbs).

🛠️ Minimal fix (use the Zone‑3 default)
-            case "validate", "verify", "check" -> "check";

103-115: Use CST modifiers instead of string search for private.

The text(classMember, source).contains("private ") pattern is fragile—it can misclassify when "private" appears in comments or strings. Use CST-based modifier detection instead, following the pattern in CstSealedErrorRule:

-        var memberText = text(classMember, source);
-        // Check if private
-        if (!memberText.contains("private ")) {
+        var isPrivate = childrenByRule(classMember, RuleId.Modifier.class).stream()
+                           .map(m -> text(m, source).trim())
+                           .anyMatch("private"::equals);
+        if (!isPrivate) {
             return false;
         }

Note: childrenByRule() retrieves only direct modifier children (preferred over findAll() for efficiency).

README.md (1)

385-420: Docs still reference JBCT-SLICE-01 after its removal.
Please update/remove the JBCT-SLICE-01 rule and slicePackages guidance to avoid misleading users.

jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstAcronymNamingRule.java (1)

54-72: Suggested fix can duplicate a boundary letter for common acronyms.
With diagnostics now using suggestFix(...), cases like HTTPClient can render as HttpCClient because the last acronym letter is processed twice. Consider skipping reprocessing of that last uppercase character.

🛠️ Proposed adjustment in suggestFix
-                        i = acronymEnd - 2;
+                        i = acronymEnd - 1;
docs/slice/architecture.md (1)

279-295: Add a language tag to the fenced code block.

markdownlint reports MD040 here; please specify a language (e.g., text) for the Slice JAR contents block.

✅ Suggested fix
-```
+```text
 org/example/order/OrderService.class
 org/example/order/OrderServiceImpl.class
 org/example/order/OrderServiceFactory.class
 org/example/order/OrderServiceFactory$orderServiceSlice.class
 org/example/order/OrderServiceFactory$inventoryService.class
 org/example/order/OrderServiceFactory$pricingEngine.class
 org/example/order/PlaceOrderRequest.class
 org/example/order/OrderResult.class
 META-INF/slice/OrderService.manifest
 META-INF/slice-api.properties
 META-INF/dependencies/org.example.order.OrderServiceFactory  # Runtime deps
 META-INF/MANIFEST.MF
   Slice-Artifact: org.example:commerce-order-service:1.0.0
   Slice-Class: org.example.order.OrderServiceFactory
 # Plus bundled external dependencies (jackson, etc.)

</details>

</blockquote></details>

</blockquote></details>
🤖 Fix all issues with AI agents
In `@CLAUDE.md`:
- Line 6: The bare URL
"https://github.com/siy/coding-technology/tree/master/proposals/rfc" in
CLAUDE.md is triggering markdownlint MD034; change it by either wrapping it in
angle brackets
(<https://github.com/siy/coding-technology/tree/master/proposals/rfc>) or
turning it into a labeled markdown link (e.g.
[proposals/rfc](https://github.com/siy/coding-technology/tree/master/proposals/rfc))
so the linter warning is cleared.

In `@jbct-core/src/main/java/org/pragmatica/jbct/config/JbctConfig.java`:
- Around line 113-147: The lambda body in JbctConfig.merge(Option<JbctConfig>
other) has inconsistent indentation; reformat the entire lambda so its body
lines (including comments and statements that set mergedFormatter, mergedLint,
mergedSourceDirs, mergedBusinessPackages, mergedSlicePackages and the return
jbctConfig(...) call) are indented consistently according to the project's rule
(lambda body aligned to the parameter + 4 spaces), ensuring comments align with
the statements they describe.

In `@jbct-core/src/main/java/org/pragmatica/jbct/format/cst/CstFormatter.java`:
- Around line 15-20: The class-level javadoc wrongly claims thread-safety while
CstFormatter holds a shared Java25Parser instance; to fix, stop reusing the
single parser field and create a fresh Java25Parser per invocation of
format(SourceFile): remove or stop referencing the shared parser field (the
constructor initialization), and instantiate a new Java25Parser inside the
format(...) method (or use a ThreadLocal supplier) so each call gets its own
parser instance and no mutable parser state is shared across threads.

In
`@jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstConstructorBypassRule.java`:
- Around line 76-81: The lambda body returned by findAncestor(root, node,
RuleId.ClassMember.class) has inconsistent indentation; reformat the block
around the lambda (the anonymous parameter "member", the var memberText =
text(member, source); line, the inline comment, the return
memberText.contains("static ") && memberText.contains("Result<"); line, and the
closing "})") so all statements inside the lambda are consistently indented and
aligned with the opening "{", preserving the comment position and keeping the
expression returned from the lambda on its own properly indented; locate this in
CstConstructorBypassRule.java by the findAncestor(...) call, the local variable
memberText and the text(member, source) invocation.

In
`@jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstSealedErrorRule.java`:
- Around line 44-46: The helper getInterfaceDecl currently falls back to
returning the parent TypeDecl via .or(typeDecl), which can mask errors; change
it to fail fast by using orElseThrow (or otherwise throw a clear
IllegalStateException) so it only returns an InterfaceDecl when one exists:
update getInterfaceDecl to call childByRule(typeDecl,
RuleId.InterfaceDecl.class).orElseThrow(...) and include a message referencing
typeDecl (or the lack of InterfaceDecl) to aid debugging; ensure any callers
that assume hasInterfaceDecl remain valid or handle the exception.

In
`@jbct-core/src/main/java/org/pragmatica/jbct/shared/GitHubContentFetcher.java`:
- Around line 46-59: Validate the repo, branch, and path inputs with the
existing UrlValidation utility before building GitHub API URLs: in
getLatestCommitSha, discoverFiles, fetchFileContent, and downloadFile call
UrlValidation (or its appropriate method) to ensure each parameter is a safe,
HTTPS/trusted-domain compliant value and reject or return a failure Result when
validation fails; only construct the URI with validated values (and disallow
characters like "../" or query fragments) so no untrusted parameter can be
injected into the GitHub API URL.

In `@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/CheckMojo.java`:
- Around line 66-68: The method call chain on getLog() in CheckMojo should align
the dotted method call with the end of the receiver; move the `.info(...)` so
the dot is directly under the end of `getLog()` (i.e., break the line after
`getLog()` and indent the next line so the `.` aligns to the last character of
`getLog()`), ensuring the chained call `info(...)` remains unchanged otherwise.

In
`@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/CollectSliceDepsMojo.java`:
- Around line 82-99: The code still reads and maps the now-unused manifest
property "api.interface": remove the dead read
props.getProperty("api.interface") and delete the conditional block that calls
mappings.setProperty(apiInterface, value) and the accompanying
getLog().debug("Found slice API...") call; update the CollectSliceDepsMojo class
Javadoc to stop claiming it extracts api.interface and state that it only
extracts impl.interface and slice.artifact instead so the documentation matches
the current ManifestGenerator output.

In
`@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/DeploySlicesMojo.java`:
- Line 101: In DeploySlicesMojo (class DeploySlicesMojo) there's a formatting
issue: the try block is written as "try{"; update it to include a space before
the brace ("try {") to match project style and other try blocks in the file.

In `@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/FormatMojo.java`:
- Around line 65-66: The chained call getLog().debug("Formatted: " + file) in
class FormatMojo should have the chaining operator aligned with the receiver to
meet coding guidelines; update the statement so the .debug(...) is placed on the
same line as the receiver end or on the following line indented to align under
getLog(), e.g., move the .debug call to align with the end of getLog() (or
extract the nested callback into a separate method if preferred for readability)
to correct the chain alignment.
- Around line 42-43: The method chaining on getLog() violates the project's
chain alignment rule; move the `.info(...)` call to its own line and align the
leading `.` under the end of the `getLog()` receiver so the chain reads as
getLog() followed by a newline and a dot-aligned `.info("Formatted: " +
formatted.get() + ", Unchanged: " + unchanged.get() + ", Errors: " +
errors.get())`; update the call in FormatMojo (referencing getLog() and
.info(...) and the formatted/unchanged/errors getters) to use that alignment.

In
`@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/GenerateBlueprintMojo.java`:
- Around line 133-155: The onPresent lambda calls
resolveExternalDependencies(...) and catches MojoExecutionException only to wrap
it in RuntimeException, which loses the checked exception and breaks Maven's
error handling; change the flow so you don't throw RuntimeException from inside
the lambda—e.g., when loadManifestFromDependency(...).onPresent(...) runs, store
the depArtifact and depManifest (or the created SliceEntry) into a temporary
collection, handle onEmpty the same way, and after the lambda processing
finishes iterate that collection and call
resolveExternalDependencies(depManifest, graph) from the outer method where you
can declare/throw MojoExecutionException (or catch it and rethrow it directly)
so the original MojoExecutionException propagates correctly; reference:
loadManifestFromDependency, resolveExternalDependencies, SliceEntry, graph.

In `@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/LintMojo.java`:
- Around line 53-55: The chained method call alignment in LintMojo
(getLog().info(...)) violates style: move the dot so the `.info(...)` call is
vertically aligned under the end of `getLog()` (i.e., place the dot at the end
of the `getLog()` line and indent the `.info(...)` line so its leading `.` lines
up with the end of `getLog()`), ensuring the call remains `getLog()` followed on
the next line by `.info("Lint results: " + errors.get() + " error(s), " +
warnings.get() + " warning(s), " + infos.get() + " info(s)");` and preserves the
original string expression.

In `@pom.xml`:
- Around line 55-58: The pom sets pragmatica-lite.version to 0.10.0 which
conflicts with the project's guideline of using 0.9.10; either confirm the
upgrade is intentional and document compatibility for modules that depend on
"pragmatica-lite" (core, http-client, toml) or revert the pom property
pragmatica-lite.version back to 0.9.10 so all modules use the guideline version;
update the <pragmatica-lite.version> property and verify dependent modules
(core, http-client, toml) build and tests pass (e.g., mvn test /
dependency:tree) to ensure no breaking changes.

In `@slice-processor/pom.xml`:
- Around line 38-43: The pom uses an unavailable Aether version: update the
aether.version Maven property from 0.8.0 to the published 0.7.4 so dependencies
like org.pragmatica-lite.aether:infra-aspect (and other Aether artifacts)
resolve; locate the <aether.version> property in the root POM and change its
value to 0.7.4 (or revert to the published version) and then run a Maven build
to verify resolution.

In
`@slice-processor/src/main/java/org/pragmatica/jbct/slice/routing/ErrorConflict.java`:
- Around line 22-24: The compact constructor in ErrorConflict uses
List.copyOf(matchingPatterns) which will NPE if matchingPatterns is null; modify
the compact constructor (ErrorConflict) to guard against null by either: 1)
validating with Objects.requireNonNull(matchingPatterns, "...") to fail fast
with a clear message, or 2) defaulting null to an empty list (e.g.,
matchingPatterns == null ? List.of() : List.copyOf(matchingPatterns)) so
matchingPatterns is never null after construction.
🧹 Nitpick comments (49)
jbct-core/src/main/java/org/pragmatica/jbct/shared/SourceFile.java (1)

18-20: Chain formatting inconsistent with coding guidelines.

The single-line chain exceeds typical line length (~115 chars) and doesn't follow the chain alignment convention (. aligns to receiver end) used elsewhere in this file (e.g., write() at lines 32-37, fileName() at lines 50-52).

♻️ Suggested formatting to align with guidelines
     public static Result<SourceFile> sourceFile(Path path) {
-        return lift(Causes::fromThrowable, () -> Files.readString(path)).map(content -> new SourceFile(path, content));
+        return lift(Causes::fromThrowable, () -> Files.readString(path))
+            .map(content -> new SourceFile(path, content));
     }

As per coding guidelines, chain alignment should have . aligned to the receiver end.

jbct-core/src/main/java/org/pragmatica/jbct/upgrade/GitHubReleaseChecker.java (1)

69-76: Chain alignment doesn't follow coding guidelines.

Per the coding guidelines, . should align to the receiver end. The .map call should align with the end of checkLatestRelease() for consistent chain alignment, similar to the pattern used in lines 57-59.

♻️ Suggested fix for chain alignment
     public Result<Option<ReleaseInfo>> checkForUpdate(String currentVersion) {
         return checkLatestRelease()
-        .map(release -> {
-                 if (isNewerVersion(currentVersion, release.version())) {
-                     return Option.option(release);
-                 }
-                 return Option.none();
-             });
+               .map(release -> {
+                   if (isNewerVersion(currentVersion, release.version())) {
+                       return Option.option(release);
+                   }
+                   return Option.none();
+               });
     }
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstAlwaysSuccessResultRule.java (2)

26-29: Chain alignment is inconsistent with coding guidelines.

The .map and .or calls should align to the receiver end. The current alignment places .map at a different indentation level than expected.

♻️ Suggested alignment
-        var packageName = findFirst(root, RuleId.PackageDecl.class).flatMap(pd -> findFirst(pd,
-                                                                                            RuleId.QualifiedName.class))
-                                   .map(qn -> text(qn, source))
-                                   .or("");
+        var packageName = findFirst(root, RuleId.PackageDecl.class)
+                                   .flatMap(pd -> findFirst(pd, RuleId.QualifiedName.class))
+                                   .map(qn -> text(qn, source))
+                                   .or("");

40-42: Chain alignment inconsistent with coding guidelines.

Per guidelines, . should align to the receiver end.

♻️ Suggested alignment
-        return childByRule(method, RuleId.Type.class).map(type -> text(type, source))
-                          .filter(typeText -> typeText.startsWith("Result<"))
-                          .isPresent();
+        return childByRule(method, RuleId.Type.class)
+                          .map(type -> text(type, source))
+                          .filter(typeText -> typeText.startsWith("Result<"))
+                          .isPresent();
jbct-core/src/main/java/org/pragmatica/jbct/init/ProjectInitializer.java (2)

91-91: Inconsistent constructor call formatting.

This constructor call has 6 arguments on a single line, while similar calls elsewhere (lines 61-66, 74-79, 101-106) wrap parameters for readability and consistency.

♻️ Suggested formatting
-        return new ProjectInitializer(projectDir, groupId, artifactId, basePackage, jbctVersion, pragmaticaVersion);
+        return new ProjectInitializer(projectDir,
+                                      groupId,
+                                      artifactId,
+                                      basePackage,
+                                      jbctVersion,
+                                      pragmaticaVersion);

114-119: Chain alignment does not follow coding guidelines.

Per the coding guidelines, . in method chains should align to the receiver end. The .flatMap should align under createDirectories(), and the inner .map should align to its receiver.

As per coding guidelines:

♻️ Suggested alignment
     public Result<List<Path>> initialize() {
         return createDirectories()
-        .flatMap(_ -> Result.all(createTemplateFiles(),
-                                 createGitkeepFiles())
-                            .map(this::combineFileLists));
+               .flatMap(_ -> Result.all(createTemplateFiles(),
+                                        createGitkeepFiles())
+                                   .map(this::combineFileLists));
     }
slice-processor/src/test/java/org/pragmatica/jbct/slice/SliceProcessorTest.java (1)

1002-1005: Verify the identity extractor pattern generates valid lambda syntax.

The assertion expects request -> request as the identity extractor. This is correct for lambda syntax, but consider also asserting the key type matches the request type to ensure the cache config is consistent.

Optional: Add assertion for consistent key/value type in identity case
         // Identity extractor (request type as key)
         assertThat(factoryContent).contains("new TypeToken<test.dto.GetUserRequest>() {}");
         assertThat(factoryContent).contains("request -> request");
+        
+        // Cache key type should match request type when using identity extractor
+        // This ensures CacheConfig<GetUserRequest, User> is generated
jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/VerifySliceMojo.java (1)

140-143: Consider extracting ternary for cleaner formatting.

The embedded ternary within string concatenation makes alignment awkward. Per coding guidelines, the ? and : should align to the opening (, but the deep nesting here reduces readability.

♻️ Suggested refactor
+            var displayScope = scope == null ? "compile" : scope;
             if (runtimeGroupIds.contains(groupId) && !allowedNonProvided.contains(artifactId)) {
                 if (!Artifact.SCOPE_PROVIDED.equals(scope) && !Artifact.SCOPE_TEST.equals(scope)) {
-                    errors.add("Dependency " + groupId + ":" + artifactId + " must have 'provided' scope. "
-                               + "Aether runtime libraries should not be bundled with slices. " + "Current scope: " + (scope == null
-                                                                                                                       ? "compile"
-                                                                                                                       : scope));
+                    errors.add("Dependency " + groupId + ":" + artifactId + " must have 'provided' scope. "
+                               + "Aether runtime libraries should not be bundled with slices. "
+                               + "Current scope: " + displayScope);
                 }
             }
jbct-core/src/main/java/org/pragmatica/jbct/init/SliceProjectValidator.java (1)

53-56: Chain alignment does not follow coding guidelines.

Per the coding guidelines, . should align to the receiver end. The .fold() should be indented to align with readFile(pomFile).

🔧 Suggested fix
         return readFile(pomFile)
-        .fold(cause -> PartialResult.error("Failed to read pom.xml: " + cause.message()),
-              content -> checkPomContent(content));
+               .fold(cause -> PartialResult.error("Failed to read pom.xml: " + cause.message()),
+                     content -> checkPomContent(content));

The same alignment issue applies to lines 78-80 and 107-109.

jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstConstructorBypassRule.java (1)

51-59: Minor formatting inconsistency with chain alignment guideline.

The .forEach on line 52 starts at column 8, which doesn't align to the receiver end (findAll(...) on line 51) as specified in the coding guidelines. Per the guidelines, . should align to the receiver end.

However, this is a minor stylistic concern and doesn't affect correctness.

♻️ Suggested alignment (optional)
         findAll(root, RuleId.RecordDecl.class)
-        .forEach(record -> {
-                     var name = childByRule(record, RuleId.Identifier.class).map(id -> text(id, source))
-                                           .or("");
-                     var recordText = text(record, source);
-                     if (recordText.contains("Result<" + name + ">")) {
-                         types.add(name);
-                     }
-                 });
+            .forEach(record -> {
+                var name = childByRule(record, RuleId.Identifier.class).map(id -> text(id, source))
+                               .or("");
+                var recordText = text(record, source);
+                if (recordText.contains("Result<" + name + ">")) {
+                    types.add(name);
+                }
+            });
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstSealedErrorRule.java (2)

31-37: Consider method reference for cleaner filter predicate.

Line 32's lambda typeDecl -> hasInterfaceDecl(typeDecl) can be simplified to a method reference.

♻️ Suggested simplification
         return findAll(root, RuleId.TypeDecl.class).stream()
-                      .filter(typeDecl -> hasInterfaceDecl(typeDecl))
+                      .filter(this::hasInterfaceDecl)
                       .filter(typeDecl -> extendsCause(typeDecl, source))
                       .filter(typeDecl -> !isSealed(typeDecl, source))

48-53: String-based heuristic may have edge-case false positives.

The contains("extends Cause") check could match:

  • Interfaces with "Cause" in their name (e.g., interface CauseMapper extends Something)
  • Comments within the interface text containing "Cause"

The first condition contains("extends Cause") is also redundant—it's always true when the second condition contains("extends ") && contains("Cause") matches with adjacent text.

This is acceptable for a lint heuristic, but worth noting as a known limitation.

♻️ Simplified condition (removes redundancy)
     private boolean extendsCause(CstNode typeDecl, String source) {
-        // Check if interface extends Cause by examining the InterfaceDecl text
-        return childByRule(typeDecl, RuleId.InterfaceDecl.class).map(iface -> text(iface, source))
-                          .map(ifaceText -> ifaceText.contains("extends Cause") || ifaceText.contains("extends ") && ifaceText.contains("Cause"))
-                          .or(false);
+        // Check if interface extends Cause by examining the InterfaceDecl text
+        return childByRule(typeDecl, RuleId.InterfaceDecl.class).map(iface -> text(iface, source))
+                          .map(ifaceText -> ifaceText.contains("extends ") && ifaceText.contains("Cause"))
+                          .or(false);
     }
jbct-cli/src/main/java/org/pragmatica/jbct/cli/UpgradeCommand.java (1)

95-99: Consider importing Path instead of using FQCN.

The fully qualified java.nio.file.Path is inconsistent with the rest of the file which uses imports. Adding the import would improve readability.

Suggested change

Add to imports section:

import java.nio.file.Path;

Then update method signature:

-    private Result<String> installScriptsAndDownload(java.nio.file.Path installDir) {
+    private Result<String> installScriptsAndDownload(Path installDir) {
jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/CollectSliceDepsMojo.java (1)

63-63: Add space before opening brace.

Standard Java formatting requires a space before the opening brace of a try block.

🎨 Suggested formatting fix
-            try{
+            try {
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstFullyQualifiedNameRule.java (1)

32-35: Chain alignment may not follow coding guidelines.

Per the coding guidelines, chain . should align to the receiver end. The current alignment of .map and .or on lines 34-35 appears inconsistent with the receiver.

♻️ Suggested alignment fix
-        var packageName = findFirst(root, RuleId.PackageDecl.class).flatMap(pd -> findFirst(pd,
-                                                                                            RuleId.QualifiedName.class))
-                                   .map(qn -> text(qn, source))
-                                   .or("");
+        var packageName = findFirst(root, RuleId.PackageDecl.class)
+                                   .flatMap(pd -> findFirst(pd, RuleId.QualifiedName.class))
+                                   .map(qn -> text(qn, source))
+                                   .or("");
jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/GenerateBlueprintMojo.java (2)

186-191: Inconsistent chain alignment.

Per coding guidelines, . should align to the receiver end. The current formatting has mixed indentation levels for the chained equals calls.

✏️ Suggested alignment fix
         for (var artifact : project.getArtifacts()) {
-            if (artifact.getGroupId()
-                        .equals(groupId) &&
-            artifact.getArtifactId()
-                    .equals(artifactId) &&
-            artifact.getVersion()
-                    .equals(version)) {
+            if (artifact.getGroupId().equals(groupId) &&
+                artifact.getArtifactId().equals(artifactId) &&
+                artifact.getVersion().equals(version)) {
                 return loadManifestFromJar(artifact.getFile());
             }
         }

321-324: Minor: Missing space before opening brace.

✏️ Formatting fix
-        try{
+        try {
             Files.createDirectories(blueprintFile.toPath()
                                                  .getParent());
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstValueObjectFactoryRule.java (1)

41-47: Consider CST-based factory detection to avoid string-match false negatives.

recordText.contains(...) can be fooled by comments/strings or formatting, and now fully gates diagnostics. Consider inspecting MethodDecl return types via CST nodes instead of substring checks. As per coding guidelines, use CST-based analysis with RuleId pattern matching.

jbct-core/src/main/java/org/pragmatica/jbct/init/GitHubVersionResolver.java (1)

98-100: Handle saveCache() failures to avoid silent cache persistence issues.

updateCache() currently ignores the Result from saveCache(), so IO failures are swallowed and you can end up repeatedly hitting GitHub without any signal. Consider at least logging or propagating that failure.

Also applies to: 102-107

jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/PackageSlicesMojo.java (3)

117-120: Duplicate JAR reading for slice dependencies.

isSliceDependency() reads the slice manifest, and then toSliceArtifactInfo() reads it again. For projects with many slice dependencies, this doubles I/O operations.

Consider caching the manifest or refactoring to return the manifest data from isSliceDependency.

♻️ Suggested refactor to avoid duplicate reads
-            } else if (isSliceDependency(artifact) && isDirectDependency) {
-                // Slice dependencies (direct only)
-                // Read actual artifact names from manifest (not Maven artifact ID)
-                sliceDeps.add(toSliceArtifactInfo(artifact));
+            } else if (isDirectDependency) {
+                var sliceManifest = readSliceManifest(artifact);
+                if (sliceManifest != null) {
+                    // Slice dependencies (direct only)
+                    // Read actual artifact names from manifest (not Maven artifact ID)
+                    sliceDeps.add(toSliceArtifactInfo(artifact, sliceManifest));
+                }

Then update toSliceArtifactInfo to accept the pre-read manifest:

private ArtifactInfo toSliceArtifactInfo(Artifact artifact, java.util.Properties props) {
    var sliceArtifact = props.getProperty("slice.artifact");
    if (sliceArtifact != null && sliceArtifact.contains(":")) {
        var parts = sliceArtifact.split(":");
        return new ArtifactInfo(parts[0], parts[1], toSemverRange(artifact.getVersion()));
    }
    return toArtifactInfo(artifact);
}

132-138: Consider importing Set and HashSet for consistency.

The file imports ArrayList and List but uses fully-qualified names for Set and HashSet. Consider adding imports for consistency.


422-433: Consider making dependency versions configurable.

The pragmatica-lite core (0.9.10) and slice-api (0.1.0) versions are hardcoded. When these dependencies are upgraded, this code must be manually updated.

Consider deriving versions from project dependencies or exposing them as plugin configuration parameters.

♻️ Example: Add plugin parameters for versions
`@Parameter`(property = "jbct.pragmaticaLiteVersion", defaultValue = "0.9.10")
private String pragmaticaLiteVersion;

`@Parameter`(property = "jbct.sliceApiVersion", defaultValue = "0.1.0")
private String sliceApiVersion;

Then use these in generatePomContent:

-        sb.append("            <version>0.9.10</version>\n");
+        sb.append("            <version>").append(pragmaticaLiteVersion).append("</version>\n");
jbct-core/src/test/java/org/pragmatica/jbct/format/GoldenFormatterTest.java (2)

127-134: Test assertions verify presence but not alignment.

The test is named chainAlignment_alignsToDotPosition but the assertions only check that chain elements are present (.contains()), not that they're properly aligned. Per the coding guidelines, chain alignment should have . aligned to the receiver end.

Consider adding an assertion that verifies the actual alignment behavior, e.g., checking that .map( appears at a specific column or that the formatted output matches an expected aligned structure.


158-167: Same observation: presence-only checks don't verify alignment.

The test argumentAlignment_alignsToOpeningParen only verifies that Result.all(, first(), second(), and third() are present, but doesn't verify they're aligned to the opening parenthesis as the test name suggests.

jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstLambdaTernaryRule.java (1)

26-29: Chain alignment doesn't follow coding guidelines.

Per the coding guidelines, chain alignment should have . aligned to the receiver end. The current formatting has inconsistent indentation:

var packageName = findFirst(root, RuleId.PackageDecl.class).flatMap(pd -> findFirst(pd,
                                                                                    RuleId.QualifiedName.class))
               .map(qn -> text(qn, source))
               .or("");

The .map() at line 28 appears to be indented to a different column than the receiver end.

♻️ Suggested alignment
-        var packageName = findFirst(root, RuleId.PackageDecl.class).flatMap(pd -> findFirst(pd,
-                                                                                            RuleId.QualifiedName.class))
-                                   .map(qn -> text(qn, source))
-                                   .or("");
+        var packageName = findFirst(root, RuleId.PackageDecl.class)
+                                   .flatMap(pd -> findFirst(pd, RuleId.QualifiedName.class))
+                                   .map(qn -> text(qn, source))
+                                   .or("");
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstConditionalLoggingRule.java (1)

45-50: Inconsistent alignment in boolean conditions.

The continuation lines in this boolean expression don't align consistently. Based on coding guidelines, the operators and subsequent conditions should align to the opening parenthesis or maintain consistent indentation.

🔧 Suggested alignment fix
-        return ( stmtText.contains("isDebugEnabled") ||
-        stmtText.contains("isTraceEnabled") ||
-        stmtText.contains("isInfoEnabled")) &&
-        (stmtText.contains(".debug(") ||
-        stmtText.contains(".trace(") ||
-        stmtText.contains(".info("));
+        return (stmtText.contains("isDebugEnabled")
+                || stmtText.contains("isTraceEnabled")
+                || stmtText.contains("isInfoEnabled"))
+               && (stmtText.contains(".debug(")
+                   || stmtText.contains(".trace(")
+                   || stmtText.contains(".info("));
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstChainLengthRule.java (1)

45-45: Minor: Unusual spacing in comparison.

The comparison != - 1 has an extra space before the negative sign. While this works, conventional style is != -1.

🔧 Suggested fix
-        while ((idx = stmtText.indexOf(".", idx)) != - 1) {
+        while ((idx = stmtText.indexOf(".", idx)) != -1) {
jbct-core/src/main/java/org/pragmatica/jbct/lint/LintConfig.java (1)

133-136: Minor redundancy in builder methods.

The builder methods withRuleSeverity and withDisabledRule call Map.copyOf/Set.copyOf before passing to the constructor, which will copy again. This is harmless but slightly wasteful.

🔧 Suggested simplification
     public LintConfig withRuleSeverity(String ruleId, DiagnosticSeverity severity) {
         var newSeverities = new HashMap<>(ruleSeverities);
         newSeverities.put(ruleId, severity);
-        return lintConfig(Map.copyOf(newSeverities), disabledRules, failOnWarning);
+        return lintConfig(newSeverities, disabledRules, failOnWarning);
     }

     public LintConfig withDisabledRule(String ruleId) {
         var newDisabled = new HashSet<>(disabledRules);
         newDisabled.add(ruleId);
-        return lintConfig(ruleSeverities, Set.copyOf(newDisabled), failOnWarning);
+        return lintConfig(ruleSeverities, newDisabled, failOnWarning);
     }

Also applies to: 142-145

jbct-core/src/test/java/org/pragmatica/jbct/init/SliceProjectInitializerTest.java (2)

44-49: Test continues after potential assertion failure.

If result.isSuccess() returns false, the assertion at line 46 will fail but the test execution continues to line 49 where Files.readString() will throw an exception because the file won't exist. This masks the actual failure cause.

Consider using the onSuccess callback pattern (like in should_create_slice_project_structure at lines 22-38) or add a guard return.

♻️ Suggested fix using guard pattern
         var result = SliceProjectInitializer.sliceProjectInitializer(projectDir, "com.test", "test-slice")
                                             .flatMap(SliceProjectInitializer::initialize);
         assertThat(result.isSuccess())
                   .as("Project initialization should succeed")
                   .isTrue();
+        if (result.isFailure()) {
+            return;
+        }
         var pomContent = Files.readString(projectDir.resolve("pom.xml"));

67-73: Same issue: test continues after potential assertion failure.

Same concern as the previous test - if initialization fails, Files.readString at line 73 will throw a misleading NoSuchFileException instead of showing the actual initialization failure.

♻️ Suggested fix
         var result = SliceProjectInitializer.sliceProjectInitializer(projectDir, "org.example", "inventory-service")
                                             .flatMap(SliceProjectInitializer::initialize);
         assertThat(result.isSuccess())
                   .as("Project initialization should succeed")
                   .isTrue();
+        if (result.isFailure()) {
+            return;
+        }
         var sliceFile = projectDir.resolve("src/main/java/org/example/inventoryservice/InventoryService.java");
jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstLambdaComplexityRule.java (3)

50-54: Control flow detection may trigger false positives on string literals.

The text-based pattern matching (lambdaText.contains("if ")) will match occurrences inside string literals, e.g., s -> log("if condition is met") would incorrectly flag as containing control flow.

Consider extracting string literals before checking, or accept this as a known limitation since complex lambdas with such strings are rare.


56-70: Statement counting includes nested blocks.

countStatements counts all semicolons within the lambda body, including those inside nested blocks (e.g., inner lambdas, anonymous classes). A lambda like s -> { list.forEach(x -> { doA(); doB(); }); } would count 3 semicolons even though the outer block has only 1 statement.

This may be acceptable since nested complexity is still complexity, but worth documenting in the class javadoc if intentional.


72-90: String escape handling doesn't account for escaped backslashes.

Line 79 checks blockBody.charAt(i - 1) != '\\' to detect escaped quotes, but this fails for "\\" (escaped backslash followed by closing quote). The sequence \\\" should close the string, but this logic would treat the quote as escaped.

For a lint rule, this edge case is rare and the impact is minor (potential false negative).

jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstNullableParameterRule.java (1)

33-34: Consider extending pattern to detect Objects.isNull/Objects.nonNull.

The regex detects param == null patterns but misses Objects.isNull(param) and Objects.nonNull(param) which are equivalent null checks. These are less common but still indicate nullable parameters.

jbct-core/src/main/java/org/pragmatica/jbct/shared/GitHubContentFetcher.java (2)

125-132: SHA extraction regex is reasonable but could be more specific.

The current pattern "sha"\s*:\s*"([^"]+)" will match the first "sha" field in the response. For the commits endpoint, this should work, but if the response structure changes or contains multiple sha fields, it may extract the wrong value.


134-141: Regex-based JSON parsing lacks robustness for production use.

The current regex approach works for GitHub's stable API format but is fragile against field reordering, escaped characters in paths, and other JSON edge cases. The project already has pragmatica-lite JSON support available through transitive dependencies.

Consider leveraging the FileInfo record already defined in this file with a proper JSON parser:

Suggested refactor
// Use pragmatica-lite JSON support (JsonMapper available via pragmatica-lite:core)
// or parse structured JSON to populate FileInfo objects
List<FileInfo> files = parseAsJson(body);  // implement JSON parsing
return files.stream()
            .filter(FileInfo::isBlob)
            .map(FileInfo::path)
            .filter(path -> path.startsWith(pathPrefix))
            .toList();
docs/slice/README.md (1)

60-66: Consider adding a language specifier to the fenced code block.

The ASCII diagram code block lacks a language specifier. Adding ```text would satisfy markdown linting rules and improve clarity.

📝 Suggested fix
-```
+```text
 `@Slice` interface -> Annotation Processor -> Generated code + manifests
                                                 |
                               Maven Plugin -> Slice JAR
                                                 |
                               Blueprint Generator -> blueprint.toml
</details>

</blockquote></details>
<details>
<summary>slice-processor/src/main/java/org/pragmatica/jbct/slice/routing/RouteSourceGenerator.java (1)</summary><blockquote>

`188-188`: **Empty catch blocks are intentional but could benefit from a comment.**

The empty catches for `IOException` and `IllegalArgumentException` are valid since the service file may not exist on first generation. A brief inline comment would clarify this is intentional.



<details>
<summary>📝 Suggested improvement</summary>

```diff
-        } catch (IOException _) {} catch (IllegalArgumentException _) {}
+        } catch (IOException | IllegalArgumentException _) {
+            // File may not exist yet - expected during first generation
+        }
jbct-core/src/main/java/org/pragmatica/jbct/update/AiToolsUpdater.java (2)

16-18: Import ordering doesn't follow guidelines.

Per coding guidelines, imports should be grouped as: org.pragmatica, java/javax, static. The SLF4J imports should be placed before the java imports.

♻️ Suggested import reordering
 import org.pragmatica.http.HttpOperations;
 import org.pragmatica.jbct.shared.GitHubContentFetcher;
 import org.pragmatica.jbct.shared.HttpClients;
 import org.pragmatica.jbct.shared.PathValidation;
 import org.pragmatica.lang.Option;
 import org.pragmatica.lang.Result;
 import org.pragmatica.lang.Unit;
 import org.pragmatica.lang.utils.Causes;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;

 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;

57-62: Consider simplifying the update-needed logic.

The current flow (filter for match → map to none → fallback to some) is correct but reads as a double-negative. A more direct approach would improve clarity.

♻️ Simpler alternative
     private Option<String> checkIfUpdateNeeded(String latestSha) {
         var currentSha = getCurrentVersion();
-        return currentSha.filter(sha -> sha.equals(latestSha))
-                         .map(_ -> Option.<String>none())
-                         .or(() -> Option.option(latestSha));
+        var isUpToDate = currentSha.filter(sha -> sha.equals(latestSha))
+                                   .isPresent();
+        return isUpToDate
+               ? Option.none()
+               : Option.some(latestSha);
     }
jbct-core/src/main/java/org/pragmatica/jbct/shared/UrlValidation.java (1)

50-54: Use Locale.ROOT for case-insensitive domain comparison.

toLowerCase() without a locale argument uses the default system locale, which can produce unexpected results with certain locales (e.g., Turkish 'I' → 'ı'). For domain name comparison, use Locale.ROOT.

♻️ Proposed fix
-        var hostLower = host.toLowerCase();
+        var hostLower = host.toLowerCase(java.util.Locale.ROOT);
slice-processor/src/main/java/org/pragmatica/jbct/slice/routing/RouteConfigLoader.java (2)

119-123: Consider using Collectors.toUnmodifiableMap instead of manual collection.

The helper can be simplified using built-in collectors.

♻️ Simplified alternative
-    private static Map<String, RouteDsl> toImmutableMap(List<Map.Entry<String, RouteDsl>> entries) {
-        var map = new HashMap<String, RouteDsl>();
-        entries.forEach(e -> map.put(e.getKey(), e.getValue()));
-        return Map.copyOf(map);
-    }
+    private static Map<String, RouteDsl> toImmutableMap(List<Map.Entry<String, RouteDsl>> entries) {
+        return entries.stream()
+                      .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey,
+                                                           Map.Entry::getValue));
+    }

98-101: Parse errors for slice config are silently discarded.

Using .option() converts parse failures to Option.none(), treating them the same as missing files. Consider logging a warning when the file exists but fails to parse, to help users diagnose configuration issues.

♻️ Add warning for parse failures
         var sliceConfig = Files.exists(slicePath)
-                          ? load(slicePath).option()
+                          ? load(slicePath)
+                                .onFailure(cause -> LOGGER.log(WARNING,
+                                                               "Failed to parse " + slicePath + ": " + cause.message()))
+                                .option()
                           : Option.<RouteConfig>none();
slice-processor/src/main/java/org/pragmatica/jbct/slice/generator/ManifestGenerator.java (3)

188-189: Line exceeds reasonable length - consider breaking up.

This single line is very long and hard to read. Consider extracting to a Set or breaking the conditions.

♻️ Improved readability
+    private static final Set<String> STANDARD_PREFIXES = Set.of("java.lang.", "java.util.");
+    private static final Set<String> PRIMITIVE_TYPES = Set.of("void", "int", "long", "boolean", "double", "float");
+
     private boolean isStandardType(String typeName) {
-        return typeName.startsWith("java.lang.") || typeName.startsWith("java.util.") || typeName.equals("void") || typeName.equals("int") || typeName.equals("long") || typeName.equals("boolean") || typeName.equals("double") || typeName.equals("float");
+        return STANDARD_PREFIXES.stream().anyMatch(typeName::startsWith)
+               || PRIMITIVE_TYPES.contains(typeName);
     }

66-70: Minor duplication with getArtifactFromEnv.

Both methods retrieve groupId and artifactId from options. Consider extracting the common base artifact computation.

♻️ Extract common logic
     private String getArtifactFromEnv() {
-        var groupId = options.getOrDefault("slice.groupId", "unknown");
-        var artifactId = options.getOrDefault("slice.artifactId", "unknown");
-        return groupId + ":" + artifactId;
+        return getBaseArtifact();
     }

     private String getSliceArtifact(String sliceName) {
+        return getBaseArtifact() + "-" + toKebabCase(sliceName);
+    }
+
+    private String getBaseArtifact() {
         var groupId = options.getOrDefault("slice.groupId", "unknown");
         var artifactId = options.getOrDefault("slice.artifactId", "unknown");
-        return groupId + ":" + artifactId + "-" + toKebabCase(sliceName);
+        return groupId + ":" + artifactId;
     }

112-117: Empty string for unresolved artifact may cause issues.

Using "" as fallback for missing sliceArtifact (line 114) while using "UNRESOLVED" for version (line 117) is inconsistent. Consider using a consistent sentinel value.

♻️ Consistent fallback values
                 props.setProperty(prefix + "artifact",
                                   resolved.sliceArtifact()
-                                          .or(() -> ""));
+                                          .or(() -> "UNRESOLVED"));
jbct-core/src/main/java/org/pragmatica/jbct/shared/SecurityError.java (1)

5-6: Remove unused imports.

URI and Path are imported but never used in this file.

🧹 Proposed fix
 package org.pragmatica.jbct.shared;
 
 import org.pragmatica.lang.Cause;
-
-import java.net.URI;
-import java.nio.file.Path;
docs/slice/development-guide.md (1)

96-99: Add language specifier to fenced code block.

The error message example should have a language specifier for consistent formatting.

📝 Proposed fix
-```
+```text
 Dependency org.pragmatica-lite:core must have 'provided' scope.
 Aether runtime libraries should not be bundled with slices.
</details>

</blockquote></details>
<details>
<summary>slice-processor/src/main/java/org/pragmatica/jbct/slice/generator/FactoryClassGenerator.java (1)</summary><blockquote>

`398-406`: **Consider validating artifact resolution and failing early in code generation.**

Using `"UNRESOLVED"` as a fallback for `fullArtifact()` generates compilable code that fails at runtime. Since artifacts are resolved during `generateFactoryClass()` before code generation begins, add validation to return `Result.failure()` from `generate()` when `fullArtifact()` is empty, preventing runtime failures with unresolved dependencies.

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

## Governing Documents

RFCs location: `../coding-technology/proposals/rfc/`
Remote: https://github.com/siy/coding-technology/tree/master/proposals/rfc
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wrap the bare URL to satisfy markdownlint (MD034).
Angle brackets or a labeled link will clear the lint warning.

✏️ Suggested fix
-Remote: https://github.com/siy/coding-technology/tree/master/proposals/rfc
+Remote: <https://github.com/siy/coding-technology/tree/master/proposals/rfc>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Remote: https://github.com/siy/coding-technology/tree/master/proposals/rfc
Remote: <https://github.com/siy/coding-technology/tree/master/proposals/rfc>
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

6-6: Bare URL used

(MD034, no-bare-urls)

🤖 Prompt for AI Agents
In `@CLAUDE.md` at line 6, The bare URL
"https://github.com/siy/coding-technology/tree/master/proposals/rfc" in
CLAUDE.md is triggering markdownlint MD034; change it by either wrapping it in
angle brackets
(<https://github.com/siy/coding-technology/tree/master/proposals/rfc>) or
turning it into a labeled markdown link (e.g.
[proposals/rfc](https://github.com/siy/coding-technology/tree/master/proposals/rfc))
so the linter warning is cleared.

Comment on lines +113 to 147
public JbctConfig merge(Option<JbctConfig> other) {
return other.map(o -> {
// Merge formatter config (use other if different from default)
var mergedFormatter = this.formatter;
if (!other.formatter.equals(FormatterConfig.DEFAULT)) {
mergedFormatter = other.formatter;
}
// Merge lint config (use other if different from default)
if (!o.formatter.equals(FormatterConfig.DEFAULT)) {
mergedFormatter = o.formatter;
}
// Merge lint config (use other if different from default)
var mergedLint = this.lint;
if (!other.lint.equals(LintConfig.DEFAULT)) {
mergedLint = other.lint;
}
// Merge source directories (use other if not default)
if (!o.lint.equals(LintConfig.DEFAULT)) {
mergedLint = o.lint;
}
// Merge source directories (use other if not default)
var mergedSourceDirs = this.sourceDirectories;
if (!other.sourceDirectories.equals(List.of("src/main/java"))) {
mergedSourceDirs = other.sourceDirectories;
}
// Merge business packages (use other if not default)
if (!o.sourceDirectories.equals(List.of("src/main/java"))) {
mergedSourceDirs = o.sourceDirectories;
}
// Merge business packages (use other if not default)
var mergedBusinessPackages = this.businessPackages;
if (!other.businessPackages.equals(List.of("**.usecase.**", "**.domain.**"))) {
mergedBusinessPackages = other.businessPackages;
}
// Merge slice packages (use other if not empty)
if (!o.businessPackages.equals(List.of("**.usecase.**", "**.domain.**"))) {
mergedBusinessPackages = o.businessPackages;
}
// Merge slice packages (use other if not empty)
var mergedSlicePackages = this.slicePackages;
if (!other.slicePackages.isEmpty()) {
mergedSlicePackages = other.slicePackages;
}
return jbctConfig(mergedFormatter, mergedLint, mergedSourceDirs, mergedBusinessPackages, mergedSlicePackages);
if (!o.slicePackages.isEmpty()) {
mergedSlicePackages = o.slicePackages;
}
return jbctConfig(mergedFormatter,
mergedLint,
mergedSourceDirs,
mergedBusinessPackages,
mergedSlicePackages);
})
.or(this);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Severe indentation inconsistency in lambda body.

The code inside the lambda has mixed indentation - some lines (e.g., var mergedFormatter, var mergedLint) are flush left relative to the lambda body while the surrounding comments and conditionals are indented differently. Per the coding guidelines, "Lambda in broken args: Body aligns to parameter + 4 spaces."

🔧 Suggested fix
     public JbctConfig merge(Option<JbctConfig> other) {
         return other.map(o -> {
-                             // Merge formatter config (use other if different from default)
-    var mergedFormatter = this.formatter;
-                             if (!o.formatter.equals(FormatterConfig.DEFAULT)) {
-                                 mergedFormatter = o.formatter;
-                             }
-                             // Merge lint config (use other if different from default)
-    var mergedLint = this.lint;
-                             if (!o.lint.equals(LintConfig.DEFAULT)) {
-                                 mergedLint = o.lint;
-                             }
-                             // Merge source directories (use other if not default)
-    var mergedSourceDirs = this.sourceDirectories;
-                             if (!o.sourceDirectories.equals(List.of("src/main/java"))) {
-                                 mergedSourceDirs = o.sourceDirectories;
-                             }
-                             // Merge business packages (use other if not default)
-    var mergedBusinessPackages = this.businessPackages;
-                             if (!o.businessPackages.equals(List.of("**.usecase.**", "**.domain.**"))) {
-                                 mergedBusinessPackages = o.businessPackages;
-                             }
-                             // Merge slice packages (use other if not empty)
-    var mergedSlicePackages = this.slicePackages;
-                             if (!o.slicePackages.isEmpty()) {
-                                 mergedSlicePackages = o.slicePackages;
-                             }
-                             return jbctConfig(mergedFormatter,
-                                               mergedLint,
-                                               mergedSourceDirs,
-                                               mergedBusinessPackages,
-                                               mergedSlicePackages);
-                         })
-                    .or(this);
+                        // Merge formatter config (use other if different from default)
+                        var mergedFormatter = this.formatter;
+                        if (!o.formatter.equals(FormatterConfig.DEFAULT)) {
+                            mergedFormatter = o.formatter;
+                        }
+                        // Merge lint config (use other if different from default)
+                        var mergedLint = this.lint;
+                        if (!o.lint.equals(LintConfig.DEFAULT)) {
+                            mergedLint = o.lint;
+                        }
+                        // Merge source directories (use other if not default)
+                        var mergedSourceDirs = this.sourceDirectories;
+                        if (!o.sourceDirectories.equals(List.of("src/main/java"))) {
+                            mergedSourceDirs = o.sourceDirectories;
+                        }
+                        // Merge business packages (use other if not default)
+                        var mergedBusinessPackages = this.businessPackages;
+                        if (!o.businessPackages.equals(List.of("**.usecase.**", "**.domain.**"))) {
+                            mergedBusinessPackages = o.businessPackages;
+                        }
+                        // Merge slice packages (use other if not empty)
+                        var mergedSlicePackages = this.slicePackages;
+                        if (!o.slicePackages.isEmpty()) {
+                            mergedSlicePackages = o.slicePackages;
+                        }
+                        return jbctConfig(mergedFormatter,
+                                          mergedLint,
+                                          mergedSourceDirs,
+                                          mergedBusinessPackages,
+                                          mergedSlicePackages);
+                    })
+                    .or(this);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public JbctConfig merge(Option<JbctConfig> other) {
return other.map(o -> {
// Merge formatter config (use other if different from default)
var mergedFormatter = this.formatter;
if (!other.formatter.equals(FormatterConfig.DEFAULT)) {
mergedFormatter = other.formatter;
}
// Merge lint config (use other if different from default)
if (!o.formatter.equals(FormatterConfig.DEFAULT)) {
mergedFormatter = o.formatter;
}
// Merge lint config (use other if different from default)
var mergedLint = this.lint;
if (!other.lint.equals(LintConfig.DEFAULT)) {
mergedLint = other.lint;
}
// Merge source directories (use other if not default)
if (!o.lint.equals(LintConfig.DEFAULT)) {
mergedLint = o.lint;
}
// Merge source directories (use other if not default)
var mergedSourceDirs = this.sourceDirectories;
if (!other.sourceDirectories.equals(List.of("src/main/java"))) {
mergedSourceDirs = other.sourceDirectories;
}
// Merge business packages (use other if not default)
if (!o.sourceDirectories.equals(List.of("src/main/java"))) {
mergedSourceDirs = o.sourceDirectories;
}
// Merge business packages (use other if not default)
var mergedBusinessPackages = this.businessPackages;
if (!other.businessPackages.equals(List.of("**.usecase.**", "**.domain.**"))) {
mergedBusinessPackages = other.businessPackages;
}
// Merge slice packages (use other if not empty)
if (!o.businessPackages.equals(List.of("**.usecase.**", "**.domain.**"))) {
mergedBusinessPackages = o.businessPackages;
}
// Merge slice packages (use other if not empty)
var mergedSlicePackages = this.slicePackages;
if (!other.slicePackages.isEmpty()) {
mergedSlicePackages = other.slicePackages;
}
return jbctConfig(mergedFormatter, mergedLint, mergedSourceDirs, mergedBusinessPackages, mergedSlicePackages);
if (!o.slicePackages.isEmpty()) {
mergedSlicePackages = o.slicePackages;
}
return jbctConfig(mergedFormatter,
mergedLint,
mergedSourceDirs,
mergedBusinessPackages,
mergedSlicePackages);
})
.or(this);
}
public JbctConfig merge(Option<JbctConfig> other) {
return other.map(o -> {
// Merge formatter config (use other if different from default)
var mergedFormatter = this.formatter;
if (!o.formatter.equals(FormatterConfig.DEFAULT)) {
mergedFormatter = o.formatter;
}
// Merge lint config (use other if different from default)
var mergedLint = this.lint;
if (!o.lint.equals(LintConfig.DEFAULT)) {
mergedLint = o.lint;
}
// Merge source directories (use other if not default)
var mergedSourceDirs = this.sourceDirectories;
if (!o.sourceDirectories.equals(List.of("src/main/java"))) {
mergedSourceDirs = o.sourceDirectories;
}
// Merge business packages (use other if not default)
var mergedBusinessPackages = this.businessPackages;
if (!o.businessPackages.equals(List.of("**.usecase.**", "**.domain.**"))) {
mergedBusinessPackages = o.businessPackages;
}
// Merge slice packages (use other if not empty)
var mergedSlicePackages = this.slicePackages;
if (!o.slicePackages.isEmpty()) {
mergedSlicePackages = o.slicePackages;
}
return jbctConfig(mergedFormatter,
mergedLint,
mergedSourceDirs,
mergedBusinessPackages,
mergedSlicePackages);
})
.or(this);
}
🤖 Prompt for AI Agents
In `@jbct-core/src/main/java/org/pragmatica/jbct/config/JbctConfig.java` around
lines 113 - 147, The lambda body in JbctConfig.merge(Option<JbctConfig> other)
has inconsistent indentation; reformat the entire lambda so its body lines
(including comments and statements that set mergedFormatter, mergedLint,
mergedSourceDirs, mergedBusinessPackages, mergedSlicePackages and the return
jbctConfig(...) call) are indented consistently according to the project's rule
(lambda body aligned to the parameter + 4 spaces), ensuring comments align with
the statements they describe.

Comment on lines +15 to +20
* <p>Uses the generated Java25Parser for parsing and preserves trivia (whitespace/comments).
*
* <p><b>Thread Safety:</b> Thread-safe for concurrent use. While the underlying parser
* and printer create per-operation state, instances of this class can be safely shared
* across threads. Each {@link #format(SourceFile)} call creates its own parser and
* printer instances internally.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and read CstFormatter.java to understand current implementation
fd -a "CstFormatter.java" . | head -1 | xargs cat -n

Repository: siy/jbct-cli

Length of output: 3801


🏁 Script executed:

#!/bin/bash
# Inspect Java25Parser for mutable state and thread-safety concerns
file=$(fd -a "Java25Parser.java" .)
if [ -n "$file" ]; then
  echo "=== Java25Parser class structure ==="
  cat -n "$file" | head -100
else
  echo "Java25Parser not found"
fi

Repository: siy/jbct-cli

Length of output: 3341


Thread-safety javadoc is incorrect and creates data race risk.

Line 17–20 claim per-call parser creation and thread-safe concurrent use. However, CstFormatter maintains a shared Java25Parser instance (line 24) initialized once in the constructor (line 28) and reused across all format() calls (line 51). Java25Parser's own javadoc explicitly states: "This class is NOT thread-safe. Each thread must use its own instance." Concurrent format() calls on the same CstFormatter instance will race on the shared parser's mutable state, violating the documented guarantee.

Fix by instantiating a new parser per call:

Proposed fix
-    private final Java25Parser parser;
-
     private CstFormatter(FormatterConfig config) {
         this.config = config;
-        this.parser = new Java25Parser();
     }

     private Result<CstNode> parse(SourceFile source) {
-        var result = parser.parseWithDiagnostics(source.content());
+        var result = new Java25Parser().parseWithDiagnostics(source.content());
🤖 Prompt for AI Agents
In `@jbct-core/src/main/java/org/pragmatica/jbct/format/cst/CstFormatter.java`
around lines 15 - 20, The class-level javadoc wrongly claims thread-safety while
CstFormatter holds a shared Java25Parser instance; to fix, stop reusing the
single parser field and create a fresh Java25Parser per invocation of
format(SourceFile): remove or stop referencing the shared parser field (the
constructor initialization), and instantiate a new Java25Parser inside the
format(...) method (or use a ThreadLocal supplier) so each call gets its own
parser instance and no mutable parser state is shared across threads.

Comment on lines +76 to 81
return findAncestor(root, node, RuleId.ClassMember.class).map(member -> {
var memberText = text(member, source);
// Allow in factory methods (static methods returning Result)
return memberText.contains("static ") && memberText.contains("Result<");
})
})
.or(false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent indentation within lambda body.

The reformatting has created visually broken indentation in this lambda:

  • Lines 77-78: Content at column ~74 (aligned after (member -> {)
  • Line 79: return statement at column 8 (minimal indentation)
  • Line 80: Closing }) at column ~70

Line 79 appears to have been missed during the reformatting, resulting in jarring inconsistency within the same block. This significantly impacts readability.

🔧 Suggested fix to align the lambda body consistently
-        return findAncestor(root, node, RuleId.ClassMember.class).map(member -> {
-                                                                          var memberText = text(member, source);
-                                                                          // Allow in factory methods (static methods returning Result)
-        return memberText.contains("static ") && memberText.contains("Result<");
-                                                                      })
-                           .or(false);
+        return findAncestor(root, node, RuleId.ClassMember.class)
+                   .map(member -> {
+                       var memberText = text(member, source);
+                       // Allow in factory methods (static methods returning Result)
+                       return memberText.contains("static ") && memberText.contains("Result<");
+                   })
+                   .or(false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return findAncestor(root, node, RuleId.ClassMember.class).map(member -> {
var memberText = text(member, source);
// Allow in factory methods (static methods returning Result)
return memberText.contains("static ") && memberText.contains("Result<");
})
})
.or(false);
return findAncestor(root, node, RuleId.ClassMember.class)
.map(member -> {
var memberText = text(member, source);
// Allow in factory methods (static methods returning Result)
return memberText.contains("static ") && memberText.contains("Result<");
})
.or(false);
🤖 Prompt for AI Agents
In
`@jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstConstructorBypassRule.java`
around lines 76 - 81, The lambda body returned by findAncestor(root, node,
RuleId.ClassMember.class) has inconsistent indentation; reformat the block
around the lambda (the anonymous parameter "member", the var memberText =
text(member, source); line, the inline comment, the return
memberText.contains("static ") && memberText.contains("Result<"); line, and the
closing "})") so all statements inside the lambda are consistently indented and
aligned with the opening "{", preserving the comment position and keeping the
expression returned from the lambda on its own properly indented; locate this in
CstConstructorBypassRule.java by the findAncestor(...) call, the local variable
memberText and the text(member, source) invocation.

Comment on lines 44 to 46
private CstNode getInterfaceDecl(CstNode typeDecl) {
return childByRule(typeDecl, RuleId.InterfaceDecl.class)
.or(typeDecl);
return childByRule(typeDecl, RuleId.InterfaceDecl.class).or(typeDecl);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fallback returns wrong node type, masking potential errors.

The .or(typeDecl) fallback returns the parent TypeDecl instead of an InterfaceDecl. While the current call site (line 35) guarantees hasInterfaceDecl is true, this fallback could cause silent failures if the method is reused or filters change.

Consider throwing or using orElseThrow() to fail fast on unexpected state.

🛡️ Defensive alternative
     private CstNode getInterfaceDecl(CstNode typeDecl) {
-        return childByRule(typeDecl, RuleId.InterfaceDecl.class).or(typeDecl);
+        return childByRule(typeDecl, RuleId.InterfaceDecl.class)
+                          .orElseThrow(() -> new IllegalStateException(
+                              "Expected InterfaceDecl child in TypeDecl"));
     }
🤖 Prompt for AI Agents
In
`@jbct-core/src/main/java/org/pragmatica/jbct/lint/cst/rules/CstSealedErrorRule.java`
around lines 44 - 46, The helper getInterfaceDecl currently falls back to
returning the parent TypeDecl via .or(typeDecl), which can mask errors; change
it to fail fast by using orElseThrow (or otherwise throw a clear
IllegalStateException) so it only returns an InterfaceDecl when one exists:
update getInterfaceDecl to call childByRule(typeDecl,
RuleId.InterfaceDecl.class).orElseThrow(...) and include a message referencing
typeDecl (or the lack of InterfaceDecl) to aid debugging; ensure any callers
that assume hasInterfaceDecl remain valid or handle the exception.

Comment on lines +133 to 155
loadManifestFromDependency(dep.artifact(),
dep.version()).onPresent(depManifest -> {
// External dependencies use default config
var entry = new SliceEntry(depArtifact,
depManifest,
SliceConfig.defaults(),
true);
graph.put(depArtifact, entry);
try{
resolveExternalDependencies(depManifest, graph);
} catch (MojoExecutionException e) {
throw new RuntimeException(e);
}
})
.onEmpty(() -> {
graph.put(depArtifact,
new SliceEntry(depArtifact,
null,
SliceConfig.defaults(),
true));
getLog().debug("No manifest found for dependency: " + depArtifact);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Checked exception wrapped in RuntimeException will bypass Maven's error handling.

The recursive call to resolveExternalDependencies inside the onPresent lambda catches MojoExecutionException and wraps it in RuntimeException. This causes Maven to display an ugly stack trace instead of a clean error message, and the exception type is lost.

Consider refactoring to collect dependencies first, then process them outside the lambda:

🔧 Suggested refactor to avoid exception wrapping
     private void resolveExternalDependencies(SliceManifest manifest,
                                              Map<String, SliceEntry> graph)
     throws MojoExecutionException {
+        var pendingResolution = new ArrayList<SliceManifest>();
+        
         for (var dep : manifest.dependencies()) {
             // Skip dependencies without artifact coordinates (local to this module)
             if (dep.artifact() == null || dep.artifact().isEmpty()) {
                 continue;
             }
             var depArtifact = dep.artifact() + ":" + dep.version();
             if (graph.containsKey(depArtifact)) {
                 continue;
             }
-            loadManifestFromDependency(dep.artifact(),
-                                       dep.version()).onPresent(depManifest -> {
-                                                                    // External dependencies use default config
-            var entry = new SliceEntry(depArtifact,
-                                       depManifest,
-                                       SliceConfig.defaults(),
-                                       true);
-                                                                    graph.put(depArtifact, entry);
-                                                                    try{
-                                                                        resolveExternalDependencies(depManifest, graph);
-                                                                    } catch (MojoExecutionException e) {
-                                                                        throw new RuntimeException(e);
-                                                                    }
-                                                                })
-                                      .onEmpty(() -> {
-                                                   graph.put(depArtifact,
-                                                             new SliceEntry(depArtifact,
-                                                                            null,
-                                                                            SliceConfig.defaults(),
-                                                                            true));
-                                                   getLog().debug("No manifest found for dependency: " + depArtifact);
-                                               });
+            var maybeManifest = loadManifestFromDependency(dep.artifact(), dep.version());
+            if (maybeManifest.isPresent()) {
+                var depManifest = maybeManifest.unwrap();
+                var entry = new SliceEntry(depArtifact, depManifest, SliceConfig.defaults(), true);
+                graph.put(depArtifact, entry);
+                pendingResolution.add(depManifest);
+            } else {
+                graph.put(depArtifact, new SliceEntry(depArtifact, null, SliceConfig.defaults(), true));
+                getLog().debug("No manifest found for dependency: " + depArtifact);
+            }
+        }
+        
+        // Recursively resolve after loop to avoid exception in lambda
+        for (var depManifest : pendingResolution) {
+            resolveExternalDependencies(depManifest, graph);
         }
     }
🤖 Prompt for AI Agents
In
`@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/GenerateBlueprintMojo.java`
around lines 133 - 155, The onPresent lambda calls
resolveExternalDependencies(...) and catches MojoExecutionException only to wrap
it in RuntimeException, which loses the checked exception and breaks Maven's
error handling; change the flow so you don't throw RuntimeException from inside
the lambda—e.g., when loadManifestFromDependency(...).onPresent(...) runs, store
the depArtifact and depManifest (or the created SliceEntry) into a temporary
collection, handle onEmpty the same way, and after the lambda processing
finishes iterate that collection and call
resolveExternalDependencies(depManifest, graph) from the outer method where you
can declare/throw MojoExecutionException (or catch it and rethrow it directly)
so the original MojoExecutionException propagates correctly; reference:
loadManifestFromDependency, resolveExternalDependencies, SliceEntry, graph.

Comment on lines 53 to +55
getLog()
.info("Lint results: " + errors.get() + " error(s), " + warnings.get() + " warning(s), " + infos.get()
+ " info(s)");
.info("Lint results: " + errors.get() + " error(s), " + warnings.get() + " warning(s), " + infos.get()
+ " info(s)");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Chain alignment violates coding guidelines.

Per coding guidelines, . should align to the receiver end. The .info(...) call should align under getLog().

Suggested fix
-        getLog()
-        .info("Lint results: " + errors.get() + " error(s), " + warnings.get() + " warning(s), " + infos.get()
-              + " info(s)");
+        getLog().info("Lint results: " + errors.get() + " error(s), " + warnings.get()
+                      + " warning(s), " + infos.get() + " info(s)");
🤖 Prompt for AI Agents
In `@jbct-maven-plugin/src/main/java/org/pragmatica/jbct/maven/LintMojo.java`
around lines 53 - 55, The chained method call alignment in LintMojo
(getLog().info(...)) violates style: move the dot so the `.info(...)` call is
vertically aligned under the end of `getLog()` (i.e., place the dot at the end
of the `getLog()` line and indent the `.info(...)` line so its leading `.` lines
up with the end of `getLog()`), ensuring the call remains `getLog()` followed on
the next line by `.info("Lint results: " + errors.get() + " error(s), " +
warnings.get() + " warning(s), " + infos.get() + " info(s)");` and preserves the
original string expression.

Comment on lines 55 to 58
<!-- Dependencies -->
<pragmatica-lite.version>0.9.11</pragmatica-lite.version>
<aether.version>0.7.5</aether.version>
<pragmatica-lite.version>0.10.0</pragmatica-lite.version>
<aether.version>0.8.0</aether.version>
<picocli.version>4.7.6</picocli.version>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the pom.xml file
fd -e pom.xml -type f | head -5

Repository: siy/jbct-cli

Length of output: 226


🏁 Script executed:

# Get Maven Central info for pragmatica-lite versions
python3 - <<'PYTHON'
import json
import urllib.request
import sys

def get_maven_versions(group, artifact):
    """Query Maven Central for available versions"""
    url = f"https://search.maven.org/solrsearch/select?q=g:%22{group}%22+AND+a:%22{artifact}%22&rows=100&wt=json"
    try:
        with urllib.request.urlopen(url, timeout=5) as response:
            data = json.loads(response.read().decode('utf-8'))
            docs = data.get("response", {}).get("docs", [])
            if docs:
                versions = sorted(set(doc.get("v") for doc in docs), reverse=True)
                return versions[:10]  # Top 10 versions
            return []
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return []

# Check pragmatica-lite artifacts
for artifact in ["core", "http-client", "toml"]:
    versions = get_maven_versions("org.pragmatica-lite", artifact)
    print(f"pragmatica-lite:{artifact} - Available versions (latest 10):")
    for v in versions:
        print(f"  {v}")
    print()
PYTHON

Repository: siy/jbct-cli

Length of output: 142


🏁 Script executed:

# Read the pom.xml file to see full dependency declarations
cat -n pom.xml | head -100

Repository: siy/jbct-cli

Length of output: 4653


🏁 Script executed:

# Continue reading pom.xml to find infra-aspect and full dependency list
tail -n +100 pom.xml | head -100

Repository: siy/jbct-cli

Length of output: 4027


🏁 Script executed:

# Search for infra-aspect in pom.xml
grep -n "infra-aspect" pom.xml

Repository: siy/jbct-cli

Length of output: 113


🏁 Script executed:

# Check if there's any Maven repository configuration or version constraints
grep -n "repository\|0.10.0" pom.xml

Repository: siy/jbct-cli

Length of output: 124


🏁 Script executed:

# Check git history or commit messages for context about this version change
git log --oneline pom.xml | head -10

Repository: siy/jbct-cli

Length of output: 132


🏁 Script executed:

# Look for any CI configuration or error logs
find . -name "*.yml" -o -name "*.yaml" | grep -E "(github|gitlab|ci|workflow)" | head -10

Repository: siy/jbct-cli

Length of output: 136


🏁 Script executed:

# Check if there are any comments or documentation about version choices
grep -B2 -A2 "0.10.0\|0.8.0" pom.xml

Repository: siy/jbct-cli

Length of output: 315


Pragmatica Lite versions conflict with coding guidelines.

Guidelines specify core/http-client/toml at 0.9.10; the pom.xml has them at 0.10.0. Confirm this upgrade is intentional and compatible, or revert to 0.9.10.

🤖 Prompt for AI Agents
In `@pom.xml` around lines 55 - 58, The pom sets pragmatica-lite.version to 0.10.0
which conflicts with the project's guideline of using 0.9.10; either confirm the
upgrade is intentional and document compatibility for modules that depend on
"pragmatica-lite" (core, http-client, toml) or revert the pom property
pragmatica-lite.version back to 0.9.10 so all modules use the guideline version;
update the <pragmatica-lite.version> property and verify dependent modules
(core, http-client, toml) build and tests pass (e.g., mvn test /
dependency:tree) to ensure no breaking changes.

Comment on lines +22 to +24
public ErrorConflict {
matchingPatterns = List.copyOf(matchingPatterns);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against null inputs in the compact constructor.

List.copyOf throws an NPE if matchingPatterns is null, which is a behavior change vs. the previous record. If null can still surface here, consider an explicit guard (or default to List.of()).

✅ Suggested fix
public ErrorConflict {
-    matchingPatterns = List.copyOf(matchingPatterns);
+    matchingPatterns = List.copyOf(java.util.Objects.requireNonNull(
+        matchingPatterns, "matchingPatterns"));
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ErrorConflict {
matchingPatterns = List.copyOf(matchingPatterns);
}
public ErrorConflict {
matchingPatterns = List.copyOf(java.util.Objects.requireNonNull(
matchingPatterns, "matchingPatterns"));
}
🤖 Prompt for AI Agents
In
`@slice-processor/src/main/java/org/pragmatica/jbct/slice/routing/ErrorConflict.java`
around lines 22 - 24, The compact constructor in ErrorConflict uses
List.copyOf(matchingPatterns) which will NPE if matchingPatterns is null; modify
the compact constructor (ErrorConflict) to guard against null by either: 1)
validating with Objects.requireNonNull(matchingPatterns, "...") to fail fast
with a clear message, or 2) defaulting null to an empty list (e.g.,
matchingPatterns == null ? List.of() : List.copyOf(matchingPatterns)) so
matchingPatterns is never null after construction.

siy added 3 commits January 23, 2026 07:25
- Bump pragmatica-lite to 0.11.1 (security fixes)
- Set aether to 0.7.5 (latest published, 0.8.1 not yet available)
- Skip slice-processor-tests module in CI (requires unreleased Aether)
- TODO: Restore slice-processor-tests when Aether 0.8.1+ published
- Add tinylog 2.7.0 to parent pom dependencyManagement
- Add tinylog-impl and slf4j-tinylog to jbct-core test scope
- Add tinylog-impl and slf4j-tinylog to slice-processor test scope
- Update jbct-cli to use version from parent (runtime scope)
- Add tinylog-test.properties for test logging configuration
- Eliminates 'No SLF4J providers found' warnings during tests
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@jbct-core/pom.xml`:
- Around line 47-56: The POM declares tinylog test-only providers but forgot to
mark them as test scope; update the two dependency entries with artifactId
tinylog-impl and slf4j-tinylog to include <scope>test</scope> so these SLF4J
bindings are only used during tests and not brought into the production
artifact.

In `@slice-processor/pom.xml`:
- Around line 82-91: The tinylog dependencies (artifactId tinylog-impl and
slf4j-tinylog) were intended as test-only SLF4J providers but lack a test scope,
causing them to be included in the compile/runtime classpath; update the two
dependency entries that reference tinylog-impl and slf4j-tinylog to add a
<scope>test</scope> element so Maven treats them as test-scoped dependencies and
they are not packaged or exposed on the compile classpath.

Comment on lines +47 to +56

<!-- Logging (SLF4J provider for tests) -->
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>tinylog-impl</artifactId>
</dependency>
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>slf4j-tinylog</artifactId>
</dependency>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing <scope>test</scope> on logging dependencies.

The comment indicates these are "SLF4J provider for tests," but neither dependency specifies test scope. Without explicit scope, Maven defaults to compile, which will:

  • Include tinylog in the production artifact unnecessarily
  • Risk SLF4J binding conflicts with downstream consumers
Proposed fix
         <!-- Logging (SLF4J provider for tests) -->
         <dependency>
             <groupId>org.tinylog</groupId>
             <artifactId>tinylog-impl</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.tinylog</groupId>
             <artifactId>slf4j-tinylog</artifactId>
+            <scope>test</scope>
         </dependency>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<!-- Logging (SLF4J provider for tests) -->
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>tinylog-impl</artifactId>
</dependency>
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>slf4j-tinylog</artifactId>
</dependency>
<!-- Logging (SLF4J provider for tests) -->
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>tinylog-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>slf4j-tinylog</artifactId>
<scope>test</scope>
</dependency>
🤖 Prompt for AI Agents
In `@jbct-core/pom.xml` around lines 47 - 56, The POM declares tinylog test-only
providers but forgot to mark them as test scope; update the two dependency
entries with artifactId tinylog-impl and slf4j-tinylog to include
<scope>test</scope> so these SLF4J bindings are only used during tests and not
brought into the production artifact.

Comment on lines +82 to +91

<!-- Logging (SLF4J provider for tests) -->
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>tinylog-impl</artifactId>
</dependency>
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>slf4j-tinylog</artifactId>
</dependency>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing <scope>test</scope> on tinylog dependencies.

The comment states these are "SLF4J provider for tests", and the commit message confirms tinylog was added as a test dependency. However, no <scope>test</scope> is specified, so Maven will default to compile scope—packaging these JARs into the final artifact and exposing them on the compile classpath.

🔧 Proposed fix
         <!-- Logging (SLF4J provider for tests) -->
         <dependency>
             <groupId>org.tinylog</groupId>
             <artifactId>tinylog-impl</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.tinylog</groupId>
             <artifactId>slf4j-tinylog</artifactId>
+            <scope>test</scope>
         </dependency>
🤖 Prompt for AI Agents
In `@slice-processor/pom.xml` around lines 82 - 91, The tinylog dependencies
(artifactId tinylog-impl and slf4j-tinylog) were intended as test-only SLF4J
providers but lack a test scope, causing them to be included in the
compile/runtime classpath; update the two dependency entries that reference
tinylog-impl and slf4j-tinylog to add a <scope>test</scope> element so Maven
treats them as test-scoped dependencies and they are not packaged or exposed on
the compile classpath.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@siy siy merged commit e51215e into main Jan 26, 2026
0 of 2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant