This commit is contained in:
Ingo Schnabel
2026-07-19 10:34:15 +02:00
parent 2a45f9fdc2
commit 0dfc85a11b
16 changed files with 467 additions and 33 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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>

View File

@@ -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)))

View File

@@ -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,

View File

@@ -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

View File

@@ -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&rarr;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")));
}
}

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -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})

View File

@@ -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
}

View 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.

View File

@@ -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).

View File

@@ -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` |

View File

@@ -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