Fixes
This commit is contained in:
@@ -7,14 +7,14 @@ import picocli.CommandLine.Parameters;
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
@Parameters(index = "0", description = "Module name")
|
||||
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 = "";
|
||||
|
||||
@Option(names = "--limit", description = "Max items to return")
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
server.url=http://localhost:8787
|
||||
# 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.
|
||||
version=77
|
||||
version=80
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
<plugin>
|
||||
<groupId>com.agenticcode</groupId>
|
||||
<artifactId>ac-mvn-plugins</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>bump-version</id>
|
||||
<goals>
|
||||
<goal>bump-version</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
@@ -532,6 +532,13 @@ public class AnalysisResource {
|
||||
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
|
||||
@Path("/modules/{name}/callers")
|
||||
@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));
|
||||
}
|
||||
|
||||
@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
|
||||
public Uni<ToolResponse> callers(
|
||||
@ToolArg(description = "Project name") String project,
|
||||
@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 offset (default 0)", required = false) @Nullable Integer offset) {
|
||||
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
|
||||
# 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).
|
||||
agenticcode.version=77
|
||||
agenticcode.version=80
|
||||
|
||||
# MCP server (HTTP/SSE transport) — tools exposed at http://<host>:8787/mcp/sse
|
||||
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>
|
||||
<version>${maven-plugin-api.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-core</artifactId>
|
||||
<version>${maven-plugin-api.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugin-tools</groupId>
|
||||
<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
|
||||
* {@link #callees(String)}. {@code scope} may be {@code "external"} (callers into the module
|
||||
* 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) {
|
||||
// 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)) {
|
||||
case "external" -> "AND target = m";
|
||||
case "internal" -> "AND target <> m";
|
||||
default -> "";
|
||||
case "internal" -> "AND target <> m AND caller <> m";
|
||||
default -> "AND target = m"; // "external" and the default
|
||||
};
|
||||
return """
|
||||
MATCH (m:AstNode {type: 'MODULE', name: $name, project: $project})
|
||||
|
||||
27
manage-ac.sh
27
manage-ac.sh
@@ -35,21 +35,12 @@ require_docker() {
|
||||
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
|
||||
# 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() {
|
||||
local props="ac-code-server/src/main/resources/application.properties"
|
||||
local cli_props="ac-cli/src/main/resources/agenticcode.properties"
|
||||
@@ -64,8 +55,8 @@ stamp_cli_version() {
|
||||
}
|
||||
|
||||
build_all() {
|
||||
bump_version
|
||||
stamp_cli_version
|
||||
# The version bump + ac-cli stamp happen inside the Maven build (ac-mvn-plugins
|
||||
# 'bump-version' mojo on ac-code-server), since 'install' is a trigger goal.
|
||||
log "Building all modules (mvn clean install -DskipTests)"
|
||||
mvn clean install -DskipTests
|
||||
}
|
||||
@@ -228,8 +219,10 @@ Usage: ./manage-ac.sh <command>
|
||||
|
||||
Notes:
|
||||
- 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
|
||||
means help rather than deploy.
|
||||
- The version bump lives in the Maven build (ac-mvn-plugins 'bump-version' mojo):
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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`
|
||||
(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.
|
||||
|
||||
## 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 /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}/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}/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` |
|
||||
|
||||
@@ -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
|
||||
corpus ever needs it.)*
|
||||
|
||||
- *Not a bug (verified):* the Overview "Called by" lists the module's own internal subroutines (from
|
||||
`context.callers`, which includes internal PERFORM callers) — noisy but accurate. Ego-graph
|
||||
`direction=in` returning 0 for `WGEAGB0S` is **correct** (nothing `CALLNAT`s it; it's a top-level XML
|
||||
entry). Payload `direction` is always `REQUEST` for PDA-derived contracts (a single interface PDA
|
||||
doesn't encode direction) — a documented limitation, not a bug.
|
||||
- [x] **Module `callers` default is external-only; no `MODULE` self-loop** (2026-07-19, WGEAGB0S deep
|
||||
API audit). Two coupled defects in `CypherQueries.callers(scope)`: (1) the top-level main body's
|
||||
`PERFORM`s originate at the `MODULE` node, so `scope=internal`/default reported the module as its own
|
||||
caller (`WGEAGB0S → WGEAGB0S`), a self-loop `callees` never mirrors — fixed with `AND caller <> m` on
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user