Fixes
This commit is contained in:
@@ -7,14 +7,14 @@ import picocli.CommandLine.Parameters;
|
|||||||
/**
|
/**
|
||||||
* Lists modules that call a given module.
|
* Lists modules that call a given module.
|
||||||
*/
|
*/
|
||||||
@Command(name = "callers", mixinStandardHelpOptions = true, description = "List callers of a module")
|
@Command(name = "callers", mixinStandardHelpOptions = true, description = "List callers of a module (default: external module callers only)")
|
||||||
final class CallersCommand extends AbstractProjectCommand {
|
final class CallersCommand extends AbstractProjectCommand {
|
||||||
|
|
||||||
@SuppressWarnings("NullAway.Init")
|
@SuppressWarnings("NullAway.Init")
|
||||||
@Parameters(index = "0", description = "Module name")
|
@Parameters(index = "0", description = "Module name")
|
||||||
String moduleName;
|
String moduleName;
|
||||||
|
|
||||||
@Option(names = "--scope", description = "Filter by call kind: 'external' (CALLNAT only) or 'internal' (PERFORM only)")
|
@Option(names = "--scope", description = "'external' (default) = module callers (CALLNAT/inheritance); 'internal' = own-subroutine PERFORM wiring")
|
||||||
String scope = "";
|
String scope = "";
|
||||||
|
|
||||||
@Option(names = "--limit", description = "Max items to return")
|
@Option(names = "--limit", description = "Max items to return")
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
server.url=http://localhost:8787
|
server.url=http://localhost:8787
|
||||||
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
|
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
|
||||||
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
|
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
|
||||||
version=77
|
version=80
|
||||||
|
|||||||
@@ -138,6 +138,14 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.agenticcode</groupId>
|
<groupId>com.agenticcode</groupId>
|
||||||
<artifactId>ac-mvn-plugins</artifactId>
|
<artifactId>ac-mvn-plugins</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>bump-version</id>
|
||||||
|
<goals>
|
||||||
|
<goal>bump-version</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
@@ -532,6 +532,13 @@ public class AnalysisResource {
|
|||||||
resp -> namesOnly(fields) ? ok(callTreeNames(resp)) : Response.ok(resp).build());
|
resp -> namesOnly(fields) ? ok(callTreeNames(resp)) : Response.ok(resp).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module-granularity callers. {@code scope}: {@code external} (default) = modules that call this
|
||||||
|
* module (incoming CALLNAT / inheritance); {@code internal} = the module's own subroutines that
|
||||||
|
* {@code PERFORM} into it. The default deliberately excludes intra-module subroutine wiring and
|
||||||
|
* never reports the module as its own caller; use {@code scope=internal} or the function-level
|
||||||
|
* {@code /functions/{fn}/callers} endpoint for that.
|
||||||
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/modules/{name}/callers")
|
@Path("/modules/{name}/callers")
|
||||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = CallRefResponse.class)))
|
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = CallRefResponse.class)))
|
||||||
|
|||||||
@@ -318,12 +318,12 @@ public class McpQueryTools {
|
|||||||
return withProject(project, () -> graphRepository.dbTableColumns(project, name).map(support::ok));
|
return withProject(project, () -> graphRepository.dbTableColumns(project, name).map(support::ok));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Tool(name = "callers", description = "Modules/functions that call the given module. 'scope' filters by edge kind.")
|
@Tool(name = "callers", description = "Callers of the given module. Default/'external' = modules that call it (CALLNAT/inheritance); 'internal' = its own subroutines' PERFORM wiring. Finer function-level callers: function_callers.")
|
||||||
@Blocking
|
@Blocking
|
||||||
public Uni<ToolResponse> callers(
|
public Uni<ToolResponse> callers(
|
||||||
@ToolArg(description = "Project name") String project,
|
@ToolArg(description = "Project name") String project,
|
||||||
@ToolArg(description = "Module name") String name,
|
@ToolArg(description = "Module name") String name,
|
||||||
@ToolArg(description = "Optional edge-kind scope filter", required = false) @Nullable String scope,
|
@ToolArg(description = "Scope: 'external' (default) = module callers; 'internal' = own-subroutine PERFORM callers", required = false) @Nullable String scope,
|
||||||
@ToolArg(description = "Page size (default 50)", required = false) @Nullable Integer limit,
|
@ToolArg(description = "Page size (default 50)", required = false) @Nullable Integer limit,
|
||||||
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
@ToolArg(description = "Page offset (default 0)", required = false) @Nullable Integer offset) {
|
||||||
return withFanoutWarm(project,
|
return withFanoutWarm(project,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ quarkus.http.port=8787
|
|||||||
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
|
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
|
||||||
# release. Single source of truth for the startup log line, GET /api/version, and the MCP
|
# release. Single source of truth for the startup log line, GET /api/version, and the MCP
|
||||||
# 'version' tool/server-info (referenced below via property expression, not duplicated).
|
# 'version' tool/server-info (referenced below via property expression, not duplicated).
|
||||||
agenticcode.version=77
|
agenticcode.version=80
|
||||||
|
|
||||||
# MCP server (HTTP/SSE transport) — tools exposed at http://<host>:8787/mcp/sse
|
# MCP server (HTTP/SSE transport) — tools exposed at http://<host>:8787/mcp/sse
|
||||||
quarkus.mcp.server.server-info.name=agenticcode
|
quarkus.mcp.server.server-info.name=agenticcode
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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.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.not;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Characterization test for the module-granularity {@code /callers} endpoint (audit WGEAGB0S).
|
||||||
|
*
|
||||||
|
* <p>Two properties are asserted, both of which failed before the fix in
|
||||||
|
* {@code CypherQueries.callers(scope)}:
|
||||||
|
* <ol>
|
||||||
|
* <li><b>No module self-loop.</b> A subprogram's top-level main body {@code PERFORM}s its own
|
||||||
|
* subroutines; those {@code CALLS} edges originate at the {@code MODULE} node itself. The old
|
||||||
|
* query matched {@code caller = m} and reported the module as its own caller
|
||||||
|
* ({@code MAINX â MAINX}), a self-loop {@code /callees} never mirrors. {@code scope=internal}
|
||||||
|
* now carries {@code AND caller <> m} and must never list the module itself.</li>
|
||||||
|
* <li><b>External-only default.</b> {@code scope=null} now defaults to {@code external}: the answer
|
||||||
|
* to "who calls this module" is the incoming CALLNAT/inheritance edges only. Intra-module
|
||||||
|
* subroutine wiring belongs to {@code scope=internal}. The old default merged both, so the
|
||||||
|
* module's own subroutines showed up as its callers.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>Fixtures: {@code MAINX} performs {@code SUB-A} from its main body, and {@code SUB-A} performs
|
||||||
|
* {@code SUB-B} (a genuine function→function internal edge). {@code CALLERX} statically
|
||||||
|
* {@code CALLNAT 'MAINX'} — the one real external caller.
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class ModuleCallersSelfLoopIT {
|
||||||
|
|
||||||
|
private static final String PROJECT = "nat-callers-self-loop";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main body performs SUB-A (caller = the module node); SUB-A performs SUB-B (caller = a FUNCTION).
|
||||||
|
*/
|
||||||
|
private static final String MAINX = """
|
||||||
|
* Subprogram whose main body performs its own subroutine.
|
||||||
|
DEFINE DATA
|
||||||
|
LOCAL
|
||||||
|
01 #X (A8)
|
||||||
|
END-DEFINE
|
||||||
|
*
|
||||||
|
PERFORM SUB-A
|
||||||
|
*
|
||||||
|
DEFINE SUBROUTINE SUB-A
|
||||||
|
PERFORM SUB-B
|
||||||
|
END-SUBROUTINE
|
||||||
|
*
|
||||||
|
DEFINE SUBROUTINE SUB-B
|
||||||
|
MOVE 'A' TO #X
|
||||||
|
END-SUBROUTINE
|
||||||
|
*
|
||||||
|
END
|
||||||
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The only genuine external caller: a static CALLNAT into MAINX.
|
||||||
|
*/
|
||||||
|
private static final String CALLERX = """
|
||||||
|
* Statically calls MAINX.
|
||||||
|
DEFINE DATA
|
||||||
|
LOCAL
|
||||||
|
01 #Y (A8)
|
||||||
|
END-DEFINE
|
||||||
|
*
|
||||||
|
CALLNAT 'MAINX'
|
||||||
|
*
|
||||||
|
END
|
||||||
|
""";
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
static Path root;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void createProject() {
|
||||||
|
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
|
||||||
|
write("MAINX.nat", MAINX);
|
||||||
|
write("CALLERX.nat", CALLERX);
|
||||||
|
|
||||||
|
given().contentType("application/json")
|
||||||
|
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natural", null, null))
|
||||||
|
.when().post("/api/projects/" + PROJECT)
|
||||||
|
.then().statusCode(201);
|
||||||
|
|
||||||
|
given().when().post("/api/projects/" + PROJECT + "/refresh?deep=true").then().statusCode(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void write(String fileName, String content) {
|
||||||
|
try {
|
||||||
|
Files.writeString(root.resolve(fileName), content);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultCallersAreExternalOnlyAndNeverIncludeTheModuleItself() {
|
||||||
|
// Default (external): the sole caller is CALLERX. Never the module itself, never a subroutine.
|
||||||
|
given().pathParam("name", "MAINX")
|
||||||
|
.when().get("/api/projects/" + PROJECT + "/modules/{name}/callers")
|
||||||
|
.then().statusCode(200)
|
||||||
|
.body("items.name", hasItem("CALLERX"))
|
||||||
|
.body("items.name", not(hasItem("MAINX")))
|
||||||
|
.body("items.name", not(hasItem("SUB-A")))
|
||||||
|
.body("items.name", not(hasItem("SUB-B")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void externalScopeReturnsOnlyRealModuleCallers() {
|
||||||
|
given().pathParam("name", "MAINX").queryParam("scope", "external")
|
||||||
|
.when().get("/api/projects/" + PROJECT + "/modules/{name}/callers")
|
||||||
|
.then().statusCode(200)
|
||||||
|
.body("items.name", hasItem("CALLERX"))
|
||||||
|
.body("items.name", not(hasItem("MAINX")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void internalScopeExcludesTheModuleSelfLoop() {
|
||||||
|
// Internal PERFORM wiring: SUB-A performs SUB-B (function->function) is present; the main
|
||||||
|
// body's PERFORM SUB-A (caller = the module node) must NOT surface the module as a caller.
|
||||||
|
given().pathParam("name", "MAINX").queryParam("scope", "internal")
|
||||||
|
.when().get("/api/projects/" + PROJECT + "/modules/{name}/callers")
|
||||||
|
.then().statusCode(200)
|
||||||
|
.body("items.name", hasItem("SUB-A"))
|
||||||
|
.body("items.name", not(hasItem("MAINX")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,12 @@
|
|||||||
<artifactId>maven-plugin-api</artifactId>
|
<artifactId>maven-plugin-api</artifactId>
|
||||||
<version>${maven-plugin-api.version}</version>
|
<version>${maven-plugin-api.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.maven</groupId>
|
||||||
|
<artifactId>maven-core</artifactId>
|
||||||
|
<version>${maven-plugin-api.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.maven.plugin-tools</groupId>
|
<groupId>org.apache.maven.plugin-tools</groupId>
|
||||||
<artifactId>maven-plugin-annotations</artifactId>
|
<artifactId>maven-plugin-annotations</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.agenticcode.version;
|
||||||
|
|
||||||
|
import org.apache.maven.execution.MavenSession;
|
||||||
|
import org.apache.maven.plugin.AbstractMojo;
|
||||||
|
import org.apache.maven.plugin.MojoExecutionException;
|
||||||
|
import org.apache.maven.plugins.annotations.LifecyclePhase;
|
||||||
|
import org.apache.maven.plugins.annotations.Mojo;
|
||||||
|
import org.apache.maven.plugins.annotations.Parameter;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments {@code agenticcode.version} (an integer build counter) in {@code application.properties}
|
||||||
|
* on every real package/install/deploy build, and stamps the same number into the ac-cli properties so
|
||||||
|
* {@code ac version} stays in lock-step with the server it was built alongside.
|
||||||
|
*
|
||||||
|
* <p>Bound to {@link LifecyclePhase#GENERATE_RESOURCES} — before {@code process-resources} copies the
|
||||||
|
* file into {@code target/classes} — so the freshly built jar already reports the bumped number. That is
|
||||||
|
* why the bump lives in the build and not in the deploy script: a plain {@code mvn clean install} bumps
|
||||||
|
* too, and the jar's baked version matches the CLI stamp.
|
||||||
|
*
|
||||||
|
* <p>{@code generate-resources} also runs for {@code mvn test}/{@code compile}, which must NOT bump.
|
||||||
|
* The mojo therefore only acts when one of {@code triggerGoals} (default {@code package,install,deploy})
|
||||||
|
* is among the session's requested goals; {@code mvn test} skips.
|
||||||
|
*/
|
||||||
|
@Mojo(name = "bump-version", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true)
|
||||||
|
public class BumpVersionMojo extends AbstractMojo {
|
||||||
|
|
||||||
|
@SuppressWarnings("NullAway.Init")
|
||||||
|
@Parameter(defaultValue = "${session}", readonly = true, required = true)
|
||||||
|
private MavenSession session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties file holding the authoritative counter.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("NullAway.Init")
|
||||||
|
@Parameter(defaultValue = "${project.basedir}/src/main/resources/application.properties", required = true)
|
||||||
|
private File versionFile;
|
||||||
|
|
||||||
|
@Parameter(defaultValue = "agenticcode.version")
|
||||||
|
private final String versionKey = "agenticcode.version";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ac-cli properties, stamped with the new counter so the CLI can detect a stale build.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("NullAway.Init")
|
||||||
|
@Parameter(defaultValue = "${project.basedir}/../ac-cli/src/main/resources/agenticcode.properties")
|
||||||
|
private File cliVersionFile;
|
||||||
|
|
||||||
|
@Parameter(defaultValue = "version")
|
||||||
|
private final String cliVersionKey = "version";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comma-separated goals that make this a real build worth bumping.
|
||||||
|
*/
|
||||||
|
@Parameter(defaultValue = "package,install,deploy")
|
||||||
|
private final String triggerGoals = "package,install,deploy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if any requested goal is a trigger. Requested goals are the CLI phases/goals (e.g.
|
||||||
|
* {@code [clean, install]}); a plugin-goal build ({@code quarkus:dev}) or {@code [test]} yields false.
|
||||||
|
*/
|
||||||
|
static boolean shouldBump(List<String> requestedGoals, Set<String> triggers) {
|
||||||
|
return requestedGoals.stream().anyMatch(triggers::contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the integer value of {@code key} from a .properties {@code content}, or throws if absent/non-numeric.
|
||||||
|
*/
|
||||||
|
static int currentValue(String content, String key) {
|
||||||
|
Matcher matcher = keyPattern(key).matcher(content);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new IllegalArgumentException("Property '" + key + "' not found");
|
||||||
|
}
|
||||||
|
return Integer.parseInt(matcher.group(1).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code content} with {@code key}'s value replaced by {@code value}, or throws if the key is absent.
|
||||||
|
*/
|
||||||
|
static String withValue(String content, String key, int value) {
|
||||||
|
Matcher matcher = keyPattern(key).matcher(content);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new IllegalArgumentException("Property '" + key + "' not found");
|
||||||
|
}
|
||||||
|
return matcher.replaceFirst(Matcher.quoteReplacement(key + "=" + value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pattern keyPattern(String key) {
|
||||||
|
return Pattern.compile("^" + Pattern.quote(key) + "=(.*)$", Pattern.MULTILINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() throws MojoExecutionException {
|
||||||
|
Set<String> triggers = Arrays.stream(triggerGoals.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(g -> !g.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
List<String> goals = session.getGoals();
|
||||||
|
if (!shouldBump(goals, triggers)) {
|
||||||
|
getLog().info("Not a package/install/deploy build (goals=" + goals + ") — version unchanged.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String content = Files.readString(versionFile.toPath());
|
||||||
|
int next = currentValue(content, versionKey) + 1;
|
||||||
|
Files.writeString(versionFile.toPath(), withValue(content, versionKey, next));
|
||||||
|
getLog().info("Bumped " + versionKey + " -> " + next + " in " + versionFile);
|
||||||
|
|
||||||
|
if (cliVersionFile.exists()) {
|
||||||
|
String cliContent = Files.readString(cliVersionFile.toPath());
|
||||||
|
Files.writeString(cliVersionFile.toPath(), withValue(cliContent, cliVersionKey, next));
|
||||||
|
getLog().info("Stamped ac-cli " + cliVersionKey + " -> " + next + " in " + cliVersionFile);
|
||||||
|
} else {
|
||||||
|
getLog().warn("ac-cli properties not found, skipping stamp: " + cliVersionFile);
|
||||||
|
}
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new MojoExecutionException("Failed to bump version in " + versionFile, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.agenticcode.version;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class BumpVersionMojoTest {
|
||||||
|
|
||||||
|
private static final Set<String> TRIGGERS = Set.of("package", "install", "deploy");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bumpsOnPackageInstallDeploy() {
|
||||||
|
assertTrue(BumpVersionMojo.shouldBump(List.of("clean", "install"), TRIGGERS));
|
||||||
|
assertTrue(BumpVersionMojo.shouldBump(List.of("package"), TRIGGERS));
|
||||||
|
assertTrue(BumpVersionMojo.shouldBump(List.of("deploy"), TRIGGERS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotBumpOnTestOrCompileOrPluginGoal() {
|
||||||
|
assertFalse(BumpVersionMojo.shouldBump(List.of("test"), TRIGGERS));
|
||||||
|
assertFalse(BumpVersionMojo.shouldBump(List.of("clean", "compile"), TRIGGERS));
|
||||||
|
assertFalse(BumpVersionMojo.shouldBump(List.of("quarkus:dev"), TRIGGERS));
|
||||||
|
assertFalse(BumpVersionMojo.shouldBump(List.of(), TRIGGERS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void currentValueReadsTheCounter() {
|
||||||
|
String content = "quarkus.http.port=8787\nagenticcode.version=77\nother=x\n";
|
||||||
|
assertEquals(77, BumpVersionMojo.currentValue(content, "agenticcode.version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withValueReplacesOnlyThatKeyAndPreservesTheRest() {
|
||||||
|
String content = "quarkus.http.port=8787\nagenticcode.version=77\nother=77\n";
|
||||||
|
String bumped = BumpVersionMojo.withValue(content, "agenticcode.version", 78);
|
||||||
|
assertEquals("quarkus.http.port=8787\nagenticcode.version=78\nother=77\n", bumped);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stampsCliStyleKey() {
|
||||||
|
String content = "version=77\n";
|
||||||
|
assertEquals("version=78\n", BumpVersionMojo.withValue(content, "version", 78));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingKeyThrows() {
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> BumpVersionMojo.currentValue("a=1\n", "agenticcode.version"));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> BumpVersionMojo.withValue("a=1\n", "agenticcode.version", 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTripIncrements() {
|
||||||
|
String content = "agenticcode.version=100\n";
|
||||||
|
int next = BumpVersionMojo.currentValue(content, "agenticcode.version") + 1;
|
||||||
|
assertEquals("agenticcode.version=101\n", BumpVersionMojo.withValue(content, "agenticcode.version", next));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1662,13 +1662,17 @@ public final class CypherQueries {
|
|||||||
* {@code m}), tagged with {@code edgeKind = EXTENDS}/{@code IMPLEMENTS} â mirroring
|
* {@code m}), tagged with {@code edgeKind = EXTENDS}/{@code IMPLEMENTS} â mirroring
|
||||||
* {@link #callees(String)}. {@code scope} may be {@code "external"} (callers into the module
|
* {@link #callees(String)}. {@code scope} may be {@code "external"} (callers into the module
|
||||||
* itself â CALLNAT and inheritance), {@code "internal"} (PERFORM into own subroutines only),
|
* itself â CALLNAT and inheritance), {@code "internal"} (PERFORM into own subroutines only),
|
||||||
* or {@code null} for all callers.
|
* or {@code null} which defaults to {@code "external"} (the module's genuine callers; internal
|
||||||
|
* subroutine wiring is reachable via {@code "internal"} or the function-level callers endpoint).
|
||||||
*/
|
*/
|
||||||
public static String callers(@Nullable String scope) {
|
public static String callers(@Nullable String scope) {
|
||||||
|
// Item: module-granularity callers. "external"/null = genuine callers of the module itself
|
||||||
|
// (incoming CALLNAT/inheritance). "internal" = PERFORM into the module's own subroutines;
|
||||||
|
// `caller <> m` drops the main body's self-loop (MODULE performing its own subroutines),
|
||||||
|
// which would otherwise surface the module as its own caller.
|
||||||
String scopeFilter = switch (scope == null ? "" : scope.toLowerCase(Locale.ROOT)) {
|
String scopeFilter = switch (scope == null ? "" : scope.toLowerCase(Locale.ROOT)) {
|
||||||
case "external" -> "AND target = m";
|
case "internal" -> "AND target <> m AND caller <> m";
|
||||||
case "internal" -> "AND target <> m";
|
default -> "AND target = m"; // "external" and the default
|
||||||
default -> "";
|
|
||||||
};
|
};
|
||||||
return """
|
return """
|
||||||
MATCH (m:AstNode {type: 'MODULE', name: $name, project: $project})
|
MATCH (m:AstNode {type: 'MODULE', name: $name, project: $project})
|
||||||
|
|||||||
27
manage-ac.sh
27
manage-ac.sh
@@ -35,21 +35,12 @@ require_docker() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
bump_version() {
|
|
||||||
local props="ac-code-server/src/main/resources/application.properties"
|
|
||||||
local current next
|
|
||||||
current="$(grep -m1 '^agenticcode.version=' "$props" | cut -d= -f2)"
|
|
||||||
if [[ -z "$current" ]]; then
|
|
||||||
echo "Could not find agenticcode.version in $props" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
next=$((current + 1))
|
|
||||||
sed -i "s/^agenticcode.version=.*/agenticcode.version=$next/" "$props"
|
|
||||||
log "Bumped agenticcode.version: $current -> $next"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Stamps ac-cli's bundled version from ac-code-server's agenticcode.version (the single
|
# Stamps ac-cli's bundled version from ac-code-server's agenticcode.version (the single
|
||||||
# source of truth), so 'ac version' can detect a stale CLI against a newer server.
|
# source of truth), so 'ac version' can detect a stale CLI against a newer server.
|
||||||
|
# Note: a full build already bumps agenticcode.version and stamps ac-cli via the
|
||||||
|
# ac-mvn-plugins 'bump-version' mojo (bound to ac-code-server's generate-resources phase).
|
||||||
|
# This function is only used by the CLI-only rebuild path, which must NOT bump but must
|
||||||
|
# still sync the CLI to the current server version.
|
||||||
stamp_cli_version() {
|
stamp_cli_version() {
|
||||||
local props="ac-code-server/src/main/resources/application.properties"
|
local props="ac-code-server/src/main/resources/application.properties"
|
||||||
local cli_props="ac-cli/src/main/resources/agenticcode.properties"
|
local cli_props="ac-cli/src/main/resources/agenticcode.properties"
|
||||||
@@ -64,8 +55,8 @@ stamp_cli_version() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_all() {
|
build_all() {
|
||||||
bump_version
|
# The version bump + ac-cli stamp happen inside the Maven build (ac-mvn-plugins
|
||||||
stamp_cli_version
|
# 'bump-version' mojo on ac-code-server), since 'install' is a trigger goal.
|
||||||
log "Building all modules (mvn clean install -DskipTests)"
|
log "Building all modules (mvn clean install -DskipTests)"
|
||||||
mvn clean install -DskipTests
|
mvn clean install -DskipTests
|
||||||
}
|
}
|
||||||
@@ -228,8 +219,10 @@ Usage: ./manage-ac.sh <command>
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- The server answers on http://localhost:8787, the UI on http://localhost:5174.
|
- The server answers on http://localhost:8787, the UI on http://localhost:5174.
|
||||||
- 'deploy' bumps agenticcode.version on every run, which is why no argument
|
- The version bump lives in the Maven build (ac-mvn-plugins 'bump-version' mojo):
|
||||||
means help rather than deploy.
|
every 'mvn package'/'install'/'deploy' bumps agenticcode.version and stamps ac-cli.
|
||||||
|
'mvn test'/'compile'/'quarkus:dev' do not. 'deploy' bumps because it runs a full
|
||||||
|
install — which is why a bare, argument-less invocation means help, not deploy.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
59
prompts/wgeagb0s-deep-api-audit.md
Normal file
59
prompts/wgeagb0s-deep-api-audit.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Deep API Audit — WGEAGB0S + all subprograms
|
||||||
|
|
||||||
|
**Preconditions:** The agentic server is up (`http://localhost:8787`) and a deep refresh of
|
||||||
|
project `upms` has completed. If the server is unreachable, stop and ask the human to run
|
||||||
|
`./manage-ac.sh deploy` — never start Docker yourself.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Perform a **very deep analysis of all agentic API endpoints**, in two tiers:
|
||||||
|
|
||||||
|
1. **Deep dive — WGEAGB0S** (worked example): ingest module **WGEAGB0S** and, for each endpoint,
|
||||||
|
determine whether the response is **correct** by **manually reading the Natural source** and
|
||||||
|
comparing it against the API output, field-by-field.
|
||||||
|
2. **Broad sweep — every subprogram in `upms`**: verify the same endpoints across **all**
|
||||||
|
subprograms, not just WGEAGB0S. Manual reading of every source is infeasible at that scale, so
|
||||||
|
drive the sweep with **automated cross-checks** (below) that catch whole classes of defects, and
|
||||||
|
escalate any module that fails a check to a manual, source-level deep dive like tier 1.
|
||||||
|
|
||||||
|
## How to work
|
||||||
|
|
||||||
|
1. **Use the REST API** (`GET /api/projects/upms/modules/{name}/...`), falling back to the `ac` CLI
|
||||||
|
for quick manual checks. Do not use MCP for this audit.
|
||||||
|
For WGEAGB0S (and any escalated module) pull every relevant endpoint: `callees`, `callers`,
|
||||||
|
`db-accesses`, `functions`, `data-structures`, `dispatch-table`, `digest`, `context`,
|
||||||
|
`call-tree`, `sql-statements`, `graph`.
|
||||||
|
2. **Read the Natural source manually** (the module plus its `USING` data areas / copycodes) and
|
||||||
|
verify each response field-by-field: call graph, DB accesses (READ/WRITE mode), variable
|
||||||
|
reads/writes, field/placeholder resolution, dispatch table. Note the sources are ISO-8859
|
||||||
|
encoded — use `grep -a` / an encoding-aware reader.
|
||||||
|
3. **Broad-sweep cross-checks (all subprograms).** Enumerate every subprogram (`GET /modules?...`,
|
||||||
|
or the `generated_src`/`user_exit`/`manual` `subprogram/` dirs) and assert API-derivable
|
||||||
|
invariants that need no per-module reading, e.g.:
|
||||||
|
- **No `MODULE` self-loop:** `callers`/`callees` never list a module as its own caller/callee.
|
||||||
|
- **`callers` default is external-only:** the default view lists incoming CALLNAT/inheritance
|
||||||
|
only — never the module's own subroutines (those belong to `scope=internal`).
|
||||||
|
- **Callee resolution matches source:** every `callees` `CALLNAT` target appears as a real
|
||||||
|
`CALLNAT '<name>'` in the module or one of its `INCLUDE`d copycodes (grep-verifiable), with
|
||||||
|
correct `viaCopycode`/`includedAt` provenance — and no phantom targets.
|
||||||
|
- **Function count parity:** `digest.functionCount` == the number of `DEFINE SUBROUTINE` in the
|
||||||
|
source; every performed subroutine exists as a `FUNCTION`.
|
||||||
|
- **DB access sanity:** modules with no `READ/FIND/STORE/UPDATE/DELETE` report empty
|
||||||
|
`db-accesses`/`sql-statements`; those with them resolve to real `DB_TABLE`s.
|
||||||
|
Rank modules by check failures; the worst offenders get a tier-1 manual deep dive.
|
||||||
|
4. **Report every discrepancy** you find. For each: the endpoint and exact wrong value, the ground
|
||||||
|
truth from the source (with line references), and the suspected root cause in the
|
||||||
|
parser/enricher/Cypher. For sweep findings, report the failing invariant, the count and list of
|
||||||
|
affected modules, and a representative worked example.
|
||||||
|
5. **Propose a concrete fix** for each error.
|
||||||
|
6. **Write failing characterization tests first** (Testcontainers ITs with minimal Natural fixtures
|
||||||
|
that reproduce the bug), then confirm they go green after the fix.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
A structured report:
|
||||||
|
|
||||||
|
- **Per-endpoint** for WGEAGB0S: PASS or the list of discrepancies with ground-truth evidence,
|
||||||
|
root-cause hypothesis, fix proposal, and the test that covers it.
|
||||||
|
- **Broad sweep:** per invariant, PASS or the affected-module count + list + a worked example, with
|
||||||
|
the same root-cause/fix/test treatment for each distinct defect class.
|
||||||
@@ -2173,3 +2173,20 @@ provably equivalent when bounded to `*0..1`, which cannot walk the cycles. 20 re
|
|||||||
`*_FOR_MODULES` batch variants). Query-only change (no `recreate`). Guard `ReadPathBoundedTraversalIT`
|
`*_FOR_MODULES` batch variants). Query-only change (no `recreate`). Guard `ReadPathBoundedTraversalIT`
|
||||||
(EXPLAIN asserts no unbounded CONTAINS expand); full IT suite 193/0/0. Live (v76): `ACCNPE01 callees`
|
(EXPLAIN asserts no unbounded CONTAINS expand); full IT suite 193/0/0. Live (v76): `ACCNPE01 callees`
|
||||||
> 2 min → 0.16 s, `digest` >10 s → 3.3 s, `context` >10 s → 3.1 s.
|
> 2 min → 0.16 s, `digest` >10 s → 3.3 s, `context` >10 s → 3.1 s.
|
||||||
|
|
||||||
|
## Version bump moved from deploy into the build — 2026-07-19
|
||||||
|
|
||||||
|
`agenticcode.version` (the integer build counter reported by `/api/version` and used by `ac version` for
|
||||||
|
stale-CLI detection) used to be incremented by `manage-ac.sh deploy` (shell `bump_version`), so a plain
|
||||||
|
`mvn clean install` never bumped and only a deploy did. It now lives in the Maven build: a new
|
||||||
|
`ac-mvn-plugins` mojo `bump-version`, bound to `ac-code-server`'s `generate-resources` phase, increments
|
||||||
|
the counter and stamps the same number into `ac-cli`'s `agenticcode.properties`. Binding to
|
||||||
|
`generate-resources` (before `process-resources`) means the freshly built jar already reports the bumped
|
||||||
|
number, keeping the jar's baked version and the CLI stamp in lock-step. The mojo only acts when a
|
||||||
|
requested goal is `package`/`install`/`deploy` (read from `MavenSession.getGoals()`), so `mvn test`,
|
||||||
|
`mvn compile` and `quarkus:dev` do not bump; `deploy` bumps because it runs a full `install`. Every such
|
||||||
|
build bumps unconditionally (no source-change gating — high numbers are harmless). `manage-ac.sh`'s
|
||||||
|
`bump_version` was removed; `stamp_cli_version` is kept only for the CLI-only rebuild path (`cli`), which
|
||||||
|
syncs the CLI to the current server version without bumping. Logic unit-tested (`BumpVersionMojoTest`,
|
||||||
|
7/0/0); live-verified: `mvn generate-resources` left the counter unchanged, `mvn package` bumped 77→78 and
|
||||||
|
stamped ac-cli 78, and `target/classes/application.properties` carried 78 (timing correct).
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ origins (`http://localhost:5173`, `http://localhost:4173`) — extend the
|
|||||||
| `GET /loc?language=&sourceFile=` | Per-language LoC/SLoC rollup (fileCount/loc/sloc) + project total; each file counted once (item 46). For a generated/user_exit project also `userExitLoc`/`userExitSloc` + `generatedExclusiveLoc`/`generatedExclusiveSloc` (item 47) |
|
| `GET /loc?language=&sourceFile=` | Per-language LoC/SLoC rollup (fileCount/loc/sloc) + project total; each file counted once (item 46). For a generated/user_exit project also `userExitLoc`/`userExitSloc` + `generatedExclusiveLoc`/`generatedExclusiveSloc` (item 47) |
|
||||||
| `GET /modules/{name}/digest` | Tiny triage view before deciding which modules to expand |
|
| `GET /modules/{name}/digest` | Tiny triage view before deciding which modules to expand |
|
||||||
| `GET /modules/{name}/context` | One-shot overview: functions, callers, callees, DB accesses, SQL/variable summaries (`?include=` for full lists) |
|
| `GET /modules/{name}/context` | One-shot overview: functions, callers, callees, DB accesses, SQL/variable summaries (`?include=` for full lists) |
|
||||||
| `GET /modules/{name}/callers` \| `/callees` | Direct callers/callees incl. `EXTENDS`/`IMPLEMENTS`/`INJECTS`/`REFERENCES` |
|
| `GET /modules/{name}/callers` \| `/callees` | Direct callers/callees incl. `EXTENDS`/`IMPLEMENTS`/`INJECTS`/`REFERENCES`. `callers` `scope`: **`external` (default)** = modules that call this one (CALLNAT/inheritance); `internal` = the module's own subroutines' `PERFORM` wiring. The default is external-only and never lists the module as its own caller (no `MODULE→MODULE` self-loop); use `scope=internal` or `/functions/{fn}/callers` for intra-module wiring. `callees` is unchanged (default lists both external CALLNAT and internal PERFORM targets) |
|
||||||
| `GET /modules/{name}/functions/{function}/callers` | **FUNCTION-level callers** (item 52): who `PERFORM`s (Natural) or calls (Java cross-class) a specific subroutine/method, with call-site `lineNos`. Finer-grained than the module-level `/callers` (which is module→module). Same `CallRefResponse` shape. MCP `function_callers`, CLI `ac function-callers <module> <function>` |
|
| `GET /modules/{name}/functions/{function}/callers` | **FUNCTION-level callers** (item 52): who `PERFORM`s (Natural) or calls (Java cross-class) a specific subroutine/method, with call-site `lineNos`. Finer-grained than the module-level `/callers` (which is module→module). Same `CallRefResponse` shape. MCP `function_callers`, CLI `ac function-callers <module> <function>` |
|
||||||
| `GET /modules/{name}/call-tree?depth=` | Transitive call graph to scope a feature |
|
| `GET /modules/{name}/call-tree?depth=` | Transitive call graph to scope a feature |
|
||||||
| `GET /modules/{name}/graph?direction=&depth=&limit=` | Ego graph (item 49): bounded module-level call neighbourhood as **nodes + edges** (unlike call-tree). `direction` = `out`/`in`/`both`; `limit` caps nodes (BFS order) and sets `truncated`; unresolved targets carry `unresolved=true` + empty `sourceFile`. MCP `ego_graph`, CLI `ac ego-graph` |
|
| `GET /modules/{name}/graph?direction=&depth=&limit=` | Ego graph (item 49): bounded module-level call neighbourhood as **nodes + edges** (unlike call-tree). `direction` = `out`/`in`/`both`; `limit` caps nodes (BFS order) and sets `truncated`; unresolved targets carry `unresolved=true` + empty `sourceFile`. MCP `ego_graph`, CLI `ac ego-graph` |
|
||||||
|
|||||||
@@ -184,11 +184,20 @@ wrong answer, found by the 2026-07-17 `VMULTMN4` audit.)*
|
|||||||
external subroutines are a language feature this parser does not resolve — worth its own item if the
|
external subroutines are a language feature this parser does not resolve — worth its own item if the
|
||||||
corpus ever needs it.)*
|
corpus ever needs it.)*
|
||||||
|
|
||||||
- *Not a bug (verified):* the Overview "Called by" lists the module's own internal subroutines (from
|
- [x] **Module `callers` default is external-only; no `MODULE` self-loop** (2026-07-19, WGEAGB0S deep
|
||||||
`context.callers`, which includes internal PERFORM callers) — noisy but accurate. Ego-graph
|
API audit). Two coupled defects in `CypherQueries.callers(scope)`: (1) the top-level main body's
|
||||||
`direction=in` returning 0 for `WGEAGB0S` is **correct** (nothing `CALLNAT`s it; it's a top-level XML
|
`PERFORM`s originate at the `MODULE` node, so `scope=internal`/default reported the module as its own
|
||||||
entry). Payload `direction` is always `REQUEST` for PDA-derived contracts (a single interface PDA
|
caller (`WGEAGB0S → WGEAGB0S`), a self-loop `callees` never mirrors — fixed with `AND caller <> m` on
|
||||||
doesn't encode direction) — a documented limitation, not a bug.
|
the internal scope; (2) the default (`scope=null`) merged external callers with intra-module PERFORM
|
||||||
|
wiring, so a module's own subroutines showed up as its "callers" — the default now maps to `external`
|
||||||
|
(genuine incoming CALLNAT/inheritance only). `context`/`digest` (both call `callers(…, null)`)
|
||||||
|
inherit the clean view; `scope=internal` still exposes function→function PERFORM wiring; callees
|
||||||
|
unchanged. Covered by `ModuleCallersSelfLoopIT` (fails 2/3 before the fix). MCP `callers` tool
|
||||||
|
description + REST endpoint doc + `mcp-api-usage-ac-implementation.md` updated.
|
||||||
|
*(Supersedes the earlier "Not a bug (verified): `context.callers` includes internal PERFORM callers —
|
||||||
|
noisy but accurate" note.)* Ego-graph `direction=in` for `WGEAGB0S` now returns its dynamic callers
|
||||||
|
`W-LST-N0`/`W-MNT-N0` (item 75). Payload `direction` is always `REQUEST` for PDA-derived contracts (a
|
||||||
|
single interface PDA doesn't encode direction) — a documented limitation, not a bug.
|
||||||
|
|
||||||
## Natural parser robustness
|
## Natural parser robustness
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user