J7+J8: resolve DB-table access through inherited Panache base classes
RiskRepository-style repositories that get their Panache-ness through a project base class (RiskRepository extends AbstractPurRepository<Risk, String>, AbstractPurRepository implements PanacheRepositoryBase) never resolved repositoryEntity, so db-accesses/sql-statements stayed empty (J7). Parser now tags such a base class with panacheEntityTypeParam + typeParams, and EXTENDS edges with typeArgs; a new resolve-panache-inherited-entity enrichment step binds them to set repositoryEntity before the existing resolve-java-db-access step runs. J8 (constant-valued @Entity(name=...)) turned out to already work via existing constant resolution — verified, no code change needed. Adds JavaDbAccessInheritedRepoIT + fixtures; updates roadmap/features/ agent-module-analysis docs.
This commit is contained in:
@@ -111,6 +111,26 @@
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- Run the *IT integration tests (QuarkusTest + Testcontainers Neo4j) in the
|
||||
integration-test/verify phases, so `mvn clean install` executes them. Requires Docker. -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.agenticcode</groupId>
|
||||
<artifactId>ac-mvn-plugins</artifactId>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.agenticcode.codeserver.api;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.RestAssured;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
|
||||
/**
|
||||
* Real-world regression for J1 that the original {@link JavaDbAccessIT} does <em>not</em> exercise.
|
||||
*
|
||||
* <p>{@code JavaDbAccessIT} only covers the "easy" shapes: a repository that <em>directly</em>
|
||||
* implements {@code PanacheRepository<Order>} (entity generic on the implements clause) and a
|
||||
* {@code @Table(name = "ORDERS")} <em>string literal</em>. The PUR {@code pur-batch} code — the
|
||||
* case that motivated J1 — hits neither, so its {@code db-accesses} came back empty even after J1a
|
||||
* shipped. This test reproduces that structure with a minimal fixture:
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>J7 — indirect Panache base:</b> {@code RiskRepository extends AbstractPurRepository<Risk,String>},
|
||||
* where {@code AbstractPurRepository … implements PanacheRepositoryBase<Entity,Id>}. The Panache
|
||||
* marker is one inheritance hop up; the entity generic is bound on the concrete repo's
|
||||
* {@code extends} clause, not on a directly-Panache supertype.</li>
|
||||
* <li><b>J8 — constant table name:</b> {@code @Entity(name = Risk.TABLE_NAME)} with
|
||||
* {@code public static final String TABLE_NAME = "RISK"} and no {@code @Table} literal.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Expected once J7 + J8 are implemented: {@code RiskImportService}'s repository calls
|
||||
* ({@code persist}/{@code findById}/{@code deleteById}) resolve to {@code READS}/{@code WRITES} on
|
||||
* the {@code RISK} table, exactly as {@link JavaDbAccessIT} asserts for {@code ORDERS}.
|
||||
*/
|
||||
@QuarkusTest
|
||||
class JavaDbAccessInheritedRepoIT {
|
||||
|
||||
private static final String PROJECT = "j7-db-access-inherited";
|
||||
|
||||
@TempDir
|
||||
static Path root;
|
||||
|
||||
@BeforeAll
|
||||
static void ingest() {
|
||||
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
|
||||
copyFixture("fixtures/java/jpa/inherited/AbstractEntity.java");
|
||||
copyFixture("fixtures/java/jpa/inherited/AbstractPurRepository.java");
|
||||
copyFixture("fixtures/java/jpa/inherited/Risk.java");
|
||||
copyFixture("fixtures/java/jpa/inherited/RiskRepository.java");
|
||||
copyFixture("fixtures/java/jpa/inherited/RiskImportService.java");
|
||||
given().contentType("application/json")
|
||||
.body(new ProjectResource.ProjectRequest(null, root.toString(), null))
|
||||
.when().post("/api/projects/" + PROJECT)
|
||||
.then().statusCode(201);
|
||||
given().when().post("/api/projects/" + PROJECT + "/ingest-all?deep=true")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
private static void copyFixture(String classpathResource) {
|
||||
String fileName = classpathResource.substring(classpathResource.lastIndexOf('/') + 1);
|
||||
try (InputStream in = JavaDbAccessInheritedRepoIT.class.getClassLoader().getResourceAsStream(classpathResource)) {
|
||||
if (in == null) {
|
||||
throw new IllegalStateException("Resource not found: " + classpathResource);
|
||||
}
|
||||
Files.write(root.resolve(fileName), in.readAllBytes());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The real-world case: a repository whose Panache-ness is inherited through a project base
|
||||
* class, backed by an entity whose table name is a constant. persist -> WRITES, findById ->
|
||||
* READS, deleteById -> WRITES(DELETE), all on RISK.
|
||||
*/
|
||||
@Test
|
||||
void inheritedRepositoryCallsResolveToEntityTable() {
|
||||
given().pathParam("name", "RiskImportService")
|
||||
.when().get("/api/projects/" + PROJECT + "/modules/{name}/db-accesses")
|
||||
.then().statusCode(200)
|
||||
.body("name", hasItem("RISK"))
|
||||
.body("findAll { it.name == 'RISK' }.mode", hasItems("READS", "WRITES"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedRepositorySqlStatementsCarryTheCallText() {
|
||||
given().pathParam("name", "RiskImportService")
|
||||
.when().get("/api/projects/" + PROJECT + "/modules/{name}/sql-statements")
|
||||
.then().statusCode(200)
|
||||
.body("table", hasItem("RISK"))
|
||||
.body("mode", hasItems("READ", "WRITE", "DELETE"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package fixtures.jpa.inherited;
|
||||
|
||||
/**
|
||||
* Minimal stand-in for PUR's {@code AbstractEntity}: the upper bound of the repository's
|
||||
* entity generic. Real world: {@code AbstractPurRepository<Entity extends AbstractEntity, Id>}.
|
||||
*/
|
||||
public abstract class AbstractEntity {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package fixtures.jpa.inherited;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
|
||||
/**
|
||||
* Mirrors PUR's {@code AbstractPurRepository}: a project-specific base class that carries the
|
||||
* Panache-ness. Concrete repositories extend <em>this</em>, so the {@code PanacheRepositoryBase}
|
||||
* marker is one inheritance hop away from the concrete repository (roadmap J7).
|
||||
*/
|
||||
public abstract class AbstractPurRepository<Entity extends AbstractEntity, Id>
|
||||
implements PanacheRepositoryBase<Entity, Id> {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package fixtures.jpa.inherited;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
|
||||
/**
|
||||
* Mirrors PUR's {@code RiskEntity}: the physical table name comes from a {@code static final}
|
||||
* constant referenced in the annotation, not a string literal, and there is no {@code @Table}
|
||||
* (the table name defaults from {@code @Entity(name=…)}) — roadmap J8.
|
||||
*/
|
||||
@Entity(name = Risk.TABLE_NAME)
|
||||
public class Risk extends AbstractEntity {
|
||||
|
||||
public static final String TABLE_NAME = "RISK";
|
||||
|
||||
Long id;
|
||||
String client;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package fixtures.jpa.inherited;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
/**
|
||||
* Mirrors PUR's {@code RiskProcessingStep}: calls the inherited-Panache repository. The
|
||||
* {@code persist}/{@code findById}/{@code deleteById} methods are inherited from
|
||||
* {@code PanacheRepositoryBase} via {@link AbstractPurRepository}.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class RiskImportService {
|
||||
|
||||
@Inject
|
||||
RiskRepository riskRepository;
|
||||
|
||||
public void save(Risk risk) {
|
||||
riskRepository.persist(risk);
|
||||
}
|
||||
|
||||
public Risk load(String id) {
|
||||
return riskRepository.findById(id);
|
||||
}
|
||||
|
||||
public void clear(String id) {
|
||||
riskRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fixtures.jpa.inherited;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
/**
|
||||
* Mirrors PUR's {@code RiskRepository}: the entity generic ({@code Risk}) is bound on the
|
||||
* {@code extends} clause of a project base class, not on a directly-Panache supertype (roadmap J7).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class RiskRepository extends AbstractPurRepository<Risk, String> {
|
||||
}
|
||||
@@ -215,6 +215,31 @@ public final class CypherQueries {
|
||||
ORDER BY module, name
|
||||
""";
|
||||
|
||||
/**
|
||||
* Resolves Panache-ness inherited through a project base class (roadmap J7): a concrete
|
||||
* repository ({@code d}) extends a project base class ({@code b}) that itself implements a
|
||||
* Panache repository base type one hop up — the parser tags {@code b} with
|
||||
* {@code panacheEntityTypeParam} (the name of {@code b}'s own type parameter that fills the
|
||||
* entity slot) and {@code typeParams} (b's own type parameter list, ordered), and tags the
|
||||
* {@code EXTENDS} edge from {@code d} with {@code typeArgs} (the concrete arguments {@code d}
|
||||
* supplies, in the same order). Finds {@code panacheEntityTypeParam}'s position in
|
||||
* {@code typeParams} and picks {@code d}'s {@code typeArgs} at that position as the concrete
|
||||
* entity, setting {@code d.repositoryEntity} so {@link #RESOLVE_JAVA_DB_ACCESS} (which must run
|
||||
* after this) resolves {@code d}'s repository calls to the entity's {@code DB_TABLE} exactly as
|
||||
* it already does for a directly-Panache repository. Only resolves one level of indirection (the
|
||||
* observed real-world shape); idempotent (plain property {@code SET}); project-wide.
|
||||
*/
|
||||
public static final String RESOLVE_PANACHE_INHERITED_ENTITY = """
|
||||
MATCH (d:AstNode {type: 'MODULE', project: $project})-[r:EXTENDS]->(b:AstNode {type: 'MODULE', project: $project})
|
||||
WHERE d.sourceFile <> "" AND b.panacheEntityTypeParam IS NOT NULL
|
||||
AND b.typeParams IS NOT NULL AND r.typeArgs IS NOT NULL
|
||||
WITH d, split(b.typeParams, ',') AS params, split(r.typeArgs, ',') AS args, b.panacheEntityTypeParam AS paramName
|
||||
UNWIND range(0, size(params) - 1) AS i
|
||||
WITH d, params[i] AS p, i, args, paramName
|
||||
WHERE p = paramName AND i < size(args)
|
||||
SET d.repositoryEntity = trim(args[i])
|
||||
""";
|
||||
|
||||
/**
|
||||
* Resolves Java JPA/Panache {@code DB_ACCESS} candidates (item J1) to the real {@code DB_TABLE}.
|
||||
* Each candidate carries {@code javaReceiverType} (the call's receiver type), the finer
|
||||
|
||||
@@ -79,6 +79,11 @@ public class GraphRepository {
|
||||
for (EdgeType type : CypherQueries.RESOLVABLE_EDGE_TYPES) {
|
||||
statements.add(new EnrichmentStep("resolve-placeholder " + type, CypherQueries.resolvePlaceholderTargets(type)));
|
||||
}
|
||||
// J7: resolve Panache-ness inherited through a project base class (needs EXTENDS pointing at
|
||||
// real nodes, i.e. after the placeholder loop above) by setting repositoryEntity on the
|
||||
// concrete repository; must run before resolve-java-db-access, which reads it. Project-wide,
|
||||
// idempotent (plain SET).
|
||||
statements.add(new EnrichmentStep("resolve-panache-inherited-entity", CypherQueries.RESOLVE_PANACHE_INHERITED_ENTITY));
|
||||
// J1: resolve Java JPA/Panache DB_ACCESS candidates to the entity's DB_TABLE (READS/WRITES for
|
||||
// db-accesses, USES_TYPE for sql-statements). Depends only on persisted MAPS_TO + node
|
||||
// properties, so it runs in every mode; project-wide and idempotent (all MERGE).
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.github.javaparser.ast.type.ClassOrInterfaceType;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Parser for Java source code based on JavaParser.
|
||||
@@ -504,10 +505,34 @@ public final class JavaParser implements LanguageParser {
|
||||
|
||||
/**
|
||||
* If {@code type} extends a known repository base type, returns its managed entity (the first
|
||||
* type argument), else {@code null}.
|
||||
* type argument), else {@code null}. Returns {@code null} (not the raw name) when that argument
|
||||
* is one of {@code type}'s own type parameters rather than a concrete class — i.e. {@code type}
|
||||
* is itself a project base class passing the entity through one level up (roadmap J7), not a
|
||||
* concrete repository; see {@link #panacheEntityTypeParam}.
|
||||
*/
|
||||
@Nullable
|
||||
private static String repositoryEntityType(ClassOrInterfaceDeclaration type) {
|
||||
String argName = firstRepositoryTypeArgName(type);
|
||||
return (argName != null && !ownTypeParamNames(type).contains(argName)) ? argName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@code type} is itself a project base class that carries Panache-ness through to its
|
||||
* subclasses (roadmap J7) — e.g. {@code AbstractPurRepository<Entity, Id> implements
|
||||
* PanacheRepositoryBase<Entity, Id>} — returns the name of {@code type}'s own type parameter
|
||||
* that fills the entity slot (e.g. {@code "Entity"}). Paired with {@code type}'s own ordered
|
||||
* type parameter list ({@link #ownTypeParamNames}), this lets the graph-side enrichment step
|
||||
* ({@code CypherQueries.RESOLVE_PANACHE_INHERITED_ENTITY}) substitute a concrete subclass's
|
||||
* type argument at the same position.
|
||||
*/
|
||||
@Nullable
|
||||
private static String panacheEntityTypeParam(ClassOrInterfaceDeclaration type) {
|
||||
String argName = firstRepositoryTypeArgName(type);
|
||||
return (argName != null && ownTypeParamNames(type).contains(argName)) ? argName : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String firstRepositoryTypeArgName(ClassOrInterfaceDeclaration type) {
|
||||
List<ClassOrInterfaceType> supertypes = new ArrayList<>(type.getExtendedTypes());
|
||||
supertypes.addAll(type.getImplementedTypes());
|
||||
for (ClassOrInterfaceType supertype : supertypes) {
|
||||
@@ -523,6 +548,26 @@ public final class JavaParser implements LanguageParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code type}'s own declared generic type parameter names, in declaration order (e.g.
|
||||
* {@code ["Entity", "Id"]} for {@code AbstractPurRepository<Entity, Id>}).
|
||||
*/
|
||||
private static List<String> ownTypeParamNames(ClassOrInterfaceDeclaration type) {
|
||||
return type.getTypeParameters().stream().map(tp -> tp.getNameAsString()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* The type arguments given on an {@code extends}/{@code implements} clause, as an edge property
|
||||
* map (roadmap J7) — e.g. {@code {typeArgs: "Risk,String"}} for {@code extends
|
||||
* AbstractPurRepository<Risk, String>}. Empty when the clause has no generic arguments.
|
||||
*/
|
||||
private static Map<String, String> extendsTypeArgs(ClassOrInterfaceType extended) {
|
||||
return extended.getTypeArguments()
|
||||
.filter(args -> !args.isEmpty())
|
||||
.map(args -> Map.of("typeArgs", args.stream().map(t -> simpleType(t.asString())).collect(Collectors.joining(","))))
|
||||
.orElse(Map.of());
|
||||
}
|
||||
|
||||
private static boolean extendsPanacheEntity(ClassOrInterfaceDeclaration type) {
|
||||
return type.getExtendedTypes().stream().anyMatch(t -> PANACHE_ENTITY_BASE_TYPES.contains(t.getNameAsString()));
|
||||
}
|
||||
@@ -689,6 +734,15 @@ public final class JavaParser implements LanguageParser {
|
||||
if (repositoryEntity != null) {
|
||||
moduleProps.put("repositoryEntity", repositoryEntity);
|
||||
}
|
||||
// J7: a project base class that passes Panache-ness one level up to its subclasses
|
||||
// (e.g. AbstractPurRepository<Entity, Id> implements PanacheRepositoryBase<Entity, Id>).
|
||||
// Tag it with its entity type parameter + own parameter list so the graph-side
|
||||
// enrichment step can bind a concrete subclass's EXTENDS type argument to it.
|
||||
@Nullable String panacheParam = panacheEntityTypeParam(type);
|
||||
if (panacheParam != null) {
|
||||
moduleProps.put("panacheEntityTypeParam", panacheParam);
|
||||
moduleProps.put("typeParams", String.join(",", ownTypeParamNames(type)));
|
||||
}
|
||||
type.getJavadoc().ifPresent(jd -> {
|
||||
String firstLine = jd.getDescription().toText().lines()
|
||||
.map(String::strip).filter(s -> !s.isEmpty()).findFirst().orElse("");
|
||||
@@ -709,7 +763,10 @@ public final class JavaParser implements LanguageParser {
|
||||
|
||||
for (ClassOrInterfaceType extended : type.getExtendedTypes()) {
|
||||
AstNode superType = referencedModule(referencedModules, nodes, extended.getNameAsString());
|
||||
edges.add(edge(EdgeType.EXTENDS, typeNode.id(), superType.id(), typeNode.startLine()));
|
||||
Map<String, String> extendsProps = extendsTypeArgs(extended);
|
||||
edges.add(extendsProps.isEmpty()
|
||||
? edge(EdgeType.EXTENDS, typeNode.id(), superType.id(), typeNode.startLine())
|
||||
: edge(EdgeType.EXTENDS, typeNode.id(), superType.id(), typeNode.startLine(), null, extendsProps));
|
||||
}
|
||||
for (ClassOrInterfaceType implemented : type.getImplementedTypes()) {
|
||||
AstNode interfaceType = referencedModule(referencedModules, nodes, implemented.getNameAsString());
|
||||
|
||||
@@ -514,11 +514,16 @@ enumerate names. Any other `fields` value (or none) returns the full shape.
|
||||
(`em.persist/merge/remove(x)`), or a Panache active-record call (`Product.findById`)
|
||||
is resolved to `READS`/`WRITES` on the entity's `@Table`. Verb→mode is heuristic
|
||||
(`save/persist/merge/update/create`→WRITE, `delete*/remove*`→WRITE shown as
|
||||
`DELETE` in `sql-statements`, `find*/get*/list*/count*`→READ). Limits: only calls
|
||||
whose entity is syntactically recoverable resolve — a custom repository with no
|
||||
generic entity type, or a `@Query`/native-SQL string, is not yet mapped (roadmap
|
||||
J1b). So an empty Java `db-accesses` means "no recognised persistence call", not
|
||||
necessarily "touches no table".
|
||||
`DELETE` in `sql-statements`, `find*/get*/list*/count*`→READ). The repository's
|
||||
entity type is recovered even when Panache-ness is inherited through a project base
|
||||
class one hop up (e.g. `RiskRepository extends AbstractPurRepository<Risk, String>`,
|
||||
where `AbstractPurRepository implements PanacheRepositoryBase`) — roadmap J7 — and the
|
||||
table name is resolved even when `@Entity(name=…)`/`@Table(name=…)` references a
|
||||
same-class `static final` constant rather than a string literal — roadmap J8. Limits:
|
||||
only calls whose entity is syntactically recoverable resolve — a custom repository with
|
||||
no generic entity type anywhere in its inheritance chain, or a `@Query`/native-SQL
|
||||
string, is not yet mapped (roadmap J1b). So an empty Java `db-accesses` means "no
|
||||
recognised persistence call", not necessarily "touches no table".
|
||||
- **Java `flow-forward`/`flow-backward` now span classes** (not just intra-class): a value
|
||||
passed as an argument to a method on another class is followed into that method's
|
||||
parameter (mapped to the *named* callee method's parameter, not every same-position
|
||||
|
||||
@@ -482,6 +482,39 @@ items). Each entry records what was built; IDs are preserved from the roadmap
|
||||
**J1b deferred** (roadmap): `@Query`/JPQL/native-SQL string parsing, derived-name
|
||||
filters, and no-generic custom repositories (need symbol resolution).
|
||||
|
||||
- [x] **J7. Panache-ness inherited through a project base class** (2026-07-07) —
|
||||
J1a only recovered `repositoryEntity` when a repository *directly* extended a
|
||||
Panache/JPA base type; a repository extending a project-specific abstract base
|
||||
(e.g. `RiskRepository extends AbstractPurRepository<Risk, String>`, where
|
||||
`AbstractPurRepository<Entity, Id> implements PanacheRepositoryBase<Entity, Id>`)
|
||||
got nothing, since the entity generic sits one inheritance hop away from the
|
||||
Panache marker. **Parser** (`JavaParser`): `repositoryEntityType` now returns
|
||||
`null` (not a bogus name) when the repository base type's first type argument is
|
||||
one of *its own* type parameters rather than a concrete class; a new
|
||||
`panacheEntityTypeParam` tags such a project base class with the name of that
|
||||
type parameter, alongside its own ordered `typeParams`; a new `typeArgs` edge
|
||||
property on `EXTENDS` records the concrete arguments a subclass supplies (e.g.
|
||||
`Risk,String`). **Enrichment** (`RESOLVE_PANACHE_INHERITED_ENTITY`, a new
|
||||
project-wide, idempotent finalize step run before `resolve-java-db-access`):
|
||||
finds the base class's `panacheEntityTypeParam` position in its `typeParams`,
|
||||
reads the subclass's `EXTENDS` `typeArgs` at that position, and `SET`s
|
||||
`repositoryEntity` on the subclass — after which `RESOLVE_JAVA_DB_ACCESS` (J1a,
|
||||
unchanged) resolves its repository calls exactly as for a directly-Panache
|
||||
repository. Resolves one level of indirection (the observed real-world shape).
|
||||
New fixtures `fixtures/java/jpa/inherited/*` and `JavaDbAccessInheritedRepoIT`
|
||||
(2 tests); full `ac-code-server` unit + integration suite green.
|
||||
|
||||
- [x] **J8. `@Entity(name=CONST)` / `@Table` with a constant table name** (verified
|
||||
2026-07-07, no change needed) — the physical table name is not always a string
|
||||
literal on the annotation; `RiskEntity`-shaped entities use a same-class constant
|
||||
reference instead (`@Entity(name = Risk.TABLE_NAME)` with
|
||||
`public static final String TABLE_NAME = "RISK";`). Turns out `resolveTableName`
|
||||
already resolved this via existing same-class constant-value resolution
|
||||
(`collectConstants`/`resolveAnnotationString`, predating J1a) — confirmed by
|
||||
parsing the `JavaDbAccessInheritedRepoIT` fixture in isolation before any code
|
||||
change. No fix required; J7 (above) was the actual blocker for that fixture's
|
||||
`db-accesses`.
|
||||
|
||||
- [x] **12. Java parser — constructors, parameters, field READS/WRITES** (2026-06-16) —
|
||||
constructors → `FUNCTION` nodes named after the class; method/constructor
|
||||
parameters → `VARIABLE` nodes (`CONTAINS` from the function); field
|
||||
|
||||
@@ -29,6 +29,11 @@ actually runs on. Ordered by analytical impact.
|
||||
see `x-docs/features.md`. Repository/EntityManager/Panache active-record calls now
|
||||
resolve to `READS`/`WRITES` on the entity's `DB_TABLE`, so Java `db-accesses`/
|
||||
`sql-statements` are populated. **J1b remains open** (below).
|
||||
- ✅ **Re-test 2026-07-07 fixed by J7 + J8** (both below): `JavaDbAccessInheritedRepoIT`
|
||||
(repo → project base → Panache base, plus a constant-valued `@Entity(name=…)`) now
|
||||
passes end-to-end — `db-accesses`/`sql-statements` resolve to `RISK`. A regression
|
||||
check against the actual 9 PUR jobs is still worth doing as a follow-up acceptance
|
||||
pass, but the two traced root causes are fixed.
|
||||
|
||||
- [ ] **J1b. Parse `@Query`/JPQL/native-SQL strings + derived-name filters (Java)**
|
||||
(found 2026-07-06) — J1a resolves calls whose entity is syntactically recoverable
|
||||
@@ -44,6 +49,33 @@ actually runs on. Ordered by analytical impact.
|
||||
`REFERENCES` (from `X.class` in argument position) edges now reconnect a job to its
|
||||
injected collaborators and referenced steps; surfaced in `callees`/`callers` as
|
||||
`edgeKind = INJECTS`/`REFERENCES`. Kept out of `call-tree` (CALLS-only) by design.
|
||||
- ✅ **Re-test 2026-07-07 confirms this works:** all 3 probed PUR job digests now list
|
||||
their steps via `callees.REFERENCES` (`RiskImportJob` → `RiskInitStep`/`RiskProcessingStep`/
|
||||
`RiskEndStep`; same for `MultiTableImportJob`, `KeyTableExportJob`), and the step's
|
||||
`callers.REFERENCES` names the job. The former job-root dead-end is fixed at digest level.
|
||||
Follow-up usability gap captured as **J9** below.
|
||||
|
||||
- [x] **J7. Panache-ness inherited through a project base class (Java)** — done 2026-07-07.
|
||||
J1a resolved DB accesses only when the repository *directly* extends a Panache type; PUR-shaped
|
||||
repositories don't (`RiskRepository extends AbstractPurRepository<Risk, String>`, with the Panache
|
||||
marker one hop up on `AbstractPurRepository<Entity, Id> implements PanacheRepositoryBase<Entity, Id>`).
|
||||
Fixed as a parser + graph-enrichment pair: the parser tags a project base class with
|
||||
`panacheEntityTypeParam` (which of its own type parameters is the entity slot) + its ordered
|
||||
`typeParams`, and tags an `EXTENDS` edge with the concrete `typeArgs` a subclass supplies; a new
|
||||
enrichment step (`resolve-panache-inherited-entity`, `CypherQueries.RESOLVE_PANACHE_INHERITED_ENTITY`)
|
||||
binds the two to set `repositoryEntity` on the concrete repository, which `RESOLVE_JAVA_DB_ACCESS`
|
||||
then uses unchanged. Covers one level of indirection (the observed shape); regression test
|
||||
`JavaDbAccessInheritedRepoIT`. Distinct from J1b's "no generic entity type at all" (the
|
||||
`IRiskRepository` interface) case.
|
||||
|
||||
- [x] **J8. `@Entity(name=CONST)` / `@Table` with a constant table name (Java)** — verified
|
||||
2026-07-07 already working (no change needed): the physical table name isn't always a string
|
||||
literal — PUR-shaped entities use a constant reference (`@Entity(name = Risk.TABLE_NAME)` with
|
||||
`public static final String TABLE_NAME = "RISK";`). `JavaParser.resolveTableName` already resolves
|
||||
same-class constant references (via `collectConstants`/`resolveAnnotationString`, from earlier
|
||||
constant-value-resolution work), so this was already handled; confirmed by direct parse of the
|
||||
`JavaDbAccessInheritedRepoIT` fixture (`Risk.java` → `DB_TABLE` node named `RISK`) before any J7 fix
|
||||
was applied.
|
||||
|
||||
- [x] **J3. Interface → implementation resolution (Java)** — done 2026-07-07, see
|
||||
`x-docs/features.md`. Explicit `IMPLEMENTED_BY` edge (interface → concrete impl) built
|
||||
@@ -72,6 +104,17 @@ actually runs on. Ordered by analytical impact.
|
||||
reporting them in `unresolved`; `target/` build output is excluded from the scan by
|
||||
default (no generated-source duplicates). So `unresolved` surfaces only genuine gaps.
|
||||
|
||||
- [ ] **J9. Opt-in traversal that follows `REFERENCES`/`INJECTS` (Java)** (found 2026-07-07) —
|
||||
J2 wired the `REFERENCES`/`INJECTS` edges but keeps them out of `call-tree` (CALLS-only). For
|
||||
framework/DI-heavy code (JBeret jobs, CDI) this means there is **no automatic transitive tree**
|
||||
from a job to its steps to their repositories — the agent must chain `digest` calls by hand
|
||||
(job-digest → step-digest → repo/entity). Mirror the J3 solution: an opt-in flag on
|
||||
`call-tree`/`callees` (e.g. `?includeEdges=REFERENCES,INJECTS` or `?followWiring=true`, REST +
|
||||
MCP) that traverses these runtime-wiring edges so `call-tree(RiskImportJob)` can reach
|
||||
`RiskProcessingStep` → `RiskRepository` in one call. Keep the CALLS-only default. Combined with
|
||||
J7/J8 this is what would finally make batch-job analysis **graph-driven rather than
|
||||
source-driven**.
|
||||
|
||||
## Token efficiency (payload shape)
|
||||
|
||||
- [x] **P1-t. `/context` is heavy by default — make sub-arrays opt-in, return counts** (done 2026-06-22)
|
||||
|
||||
Reference in New Issue
Block a user