From 0dfc85a11bedf1948220e584fcafc8740fc0500f Mon Sep 17 00:00:00 2001 From: Ingo Schnabel Date: Sun, 19 Jul 2026 10:34:15 +0200 Subject: [PATCH] Fixes --- .../com/agenticcode/cli/CallersCommand.java | 4 +- .../src/main/resources/agenticcode.properties | 2 +- ac-code-server/pom.xml | 8 + .../codeserver/api/AnalysisResource.java | 7 + .../codeserver/mcp/McpQueryTools.java | 4 +- .../src/main/resources/application.properties | 2 +- .../api/ModuleCallersSelfLoopIT.java | 138 ++++++++++++++++++ ac-mvn-plugins/pom.xml | 6 + .../agenticcode/version/BumpVersionMojo.java | 131 +++++++++++++++++ .../version/BumpVersionMojoTest.java | 62 ++++++++ .../neo4jstore/graph/CypherQueries.java | 12 +- manage-ac.sh | 27 ++-- prompts/wgeagb0s-deep-api-audit.md | 59 ++++++++ x-docs/features.md | 17 +++ x-docs/mcp-api-usage-ac-implementation.md | 2 +- x-docs/roadmap.md | 19 ++- 16 files changed, 467 insertions(+), 33 deletions(-) create mode 100644 ac-code-server/src/test/java/com/agenticcode/codeserver/api/ModuleCallersSelfLoopIT.java create mode 100644 ac-mvn-plugins/src/main/java/com/agenticcode/version/BumpVersionMojo.java create mode 100644 ac-mvn-plugins/src/test/java/com/agenticcode/version/BumpVersionMojoTest.java create mode 100644 prompts/wgeagb0s-deep-api-audit.md diff --git a/ac-cli/src/main/java/com/agenticcode/cli/CallersCommand.java b/ac-cli/src/main/java/com/agenticcode/cli/CallersCommand.java index 667b6df..22576e5 100644 --- a/ac-cli/src/main/java/com/agenticcode/cli/CallersCommand.java +++ b/ac-cli/src/main/java/com/agenticcode/cli/CallersCommand.java @@ -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") diff --git a/ac-cli/src/main/resources/agenticcode.properties b/ac-cli/src/main/resources/agenticcode.properties index b8c5b9f..ad5d632 100644 --- a/ac-cli/src/main/resources/agenticcode.properties +++ b/ac-cli/src/main/resources/agenticcode.properties @@ -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 diff --git a/ac-code-server/pom.xml b/ac-code-server/pom.xml index 6793a4e..fe6626c 100644 --- a/ac-code-server/pom.xml +++ b/ac-code-server/pom.xml @@ -138,6 +138,14 @@ com.agenticcode ac-mvn-plugins + + + bump-version + + bump-version + + + diff --git a/ac-code-server/src/main/java/com/agenticcode/codeserver/api/AnalysisResource.java b/ac-code-server/src/main/java/com/agenticcode/codeserver/api/AnalysisResource.java index a9e444d..cfc1171 100644 --- a/ac-code-server/src/main/java/com/agenticcode/codeserver/api/AnalysisResource.java +++ b/ac-code-server/src/main/java/com/agenticcode/codeserver/api/AnalysisResource.java @@ -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))) diff --git a/ac-code-server/src/main/java/com/agenticcode/codeserver/mcp/McpQueryTools.java b/ac-code-server/src/main/java/com/agenticcode/codeserver/mcp/McpQueryTools.java index 101f26a..55bd67d 100644 --- a/ac-code-server/src/main/java/com/agenticcode/codeserver/mcp/McpQueryTools.java +++ b/ac-code-server/src/main/java/com/agenticcode/codeserver/mcp/McpQueryTools.java @@ -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 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, diff --git a/ac-code-server/src/main/resources/application.properties b/ac-code-server/src/main/resources/application.properties index 9bfdbab..04e371c 100644 --- a/ac-code-server/src/main/resources/application.properties +++ b/ac-code-server/src/main/resources/application.properties @@ -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://:8787/mcp/sse quarkus.mcp.server.server-info.name=agenticcode diff --git a/ac-code-server/src/test/java/com/agenticcode/codeserver/api/ModuleCallersSelfLoopIT.java b/ac-code-server/src/test/java/com/agenticcode/codeserver/api/ModuleCallersSelfLoopIT.java new file mode 100644 index 0000000..0f52efe --- /dev/null +++ b/ac-code-server/src/test/java/com/agenticcode/codeserver/api/ModuleCallersSelfLoopIT.java @@ -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). + * + *

Two properties are asserted, both of which failed before the fix in + * {@code CypherQueries.callers(scope)}: + *

    + *
  1. No module self-loop. 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.
  2. + *
  3. External-only default. {@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.
  4. + *
+ * + *

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"))); + } +} diff --git a/ac-mvn-plugins/pom.xml b/ac-mvn-plugins/pom.xml index e4b861a..8e4e4cc 100644 --- a/ac-mvn-plugins/pom.xml +++ b/ac-mvn-plugins/pom.xml @@ -28,6 +28,12 @@ maven-plugin-api ${maven-plugin-api.version} + + org.apache.maven + maven-core + ${maven-plugin-api.version} + provided + org.apache.maven.plugin-tools maven-plugin-annotations diff --git a/ac-mvn-plugins/src/main/java/com/agenticcode/version/BumpVersionMojo.java b/ac-mvn-plugins/src/main/java/com/agenticcode/version/BumpVersionMojo.java new file mode 100644 index 0000000..d315c85 --- /dev/null +++ b/ac-mvn-plugins/src/main/java/com/agenticcode/version/BumpVersionMojo.java @@ -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. + * + *

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. + * + *

{@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 requestedGoals, Set 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 triggers = Arrays.stream(triggerGoals.split(",")) + .map(String::trim) + .filter(g -> !g.isEmpty()) + .collect(Collectors.toSet()); + + List 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); + } + } +} diff --git a/ac-mvn-plugins/src/test/java/com/agenticcode/version/BumpVersionMojoTest.java b/ac-mvn-plugins/src/test/java/com/agenticcode/version/BumpVersionMojoTest.java new file mode 100644 index 0000000..5f71a31 --- /dev/null +++ b/ac-mvn-plugins/src/test/java/com/agenticcode/version/BumpVersionMojoTest.java @@ -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 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)); + } +} diff --git a/ac-neo4j-store/src/main/java/com/agenticcode/neo4jstore/graph/CypherQueries.java b/ac-neo4j-store/src/main/java/com/agenticcode/neo4jstore/graph/CypherQueries.java index b605873..0d8f662 100644 --- a/ac-neo4j-store/src/main/java/com/agenticcode/neo4jstore/graph/CypherQueries.java +++ b/ac-neo4j-store/src/main/java/com/agenticcode/neo4jstore/graph/CypherQueries.java @@ -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}) diff --git a/manage-ac.sh b/manage-ac.sh index 2d9db57..ff4a7e1 100755 --- a/manage-ac.sh +++ b/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 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 } diff --git a/prompts/wgeagb0s-deep-api-audit.md b/prompts/wgeagb0s-deep-api-audit.md new file mode 100644 index 0000000..016d491 --- /dev/null +++ b/prompts/wgeagb0s-deep-api-audit.md @@ -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 ''` 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. diff --git a/x-docs/features.md b/x-docs/features.md index 3bf917a..62ffdb7 100644 --- a/x-docs/features.md +++ b/x-docs/features.md @@ -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). diff --git a/x-docs/mcp-api-usage-ac-implementation.md b/x-docs/mcp-api-usage-ac-implementation.md index 1d1b986..66078af 100644 --- a/x-docs/mcp-api-usage-ac-implementation.md +++ b/x-docs/mcp-api-usage-ac-implementation.md @@ -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 ` | | `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` | diff --git a/x-docs/roadmap.md b/x-docs/roadmap.md index 8c68f25..a56168e 100644 --- a/x-docs/roadmap.md +++ b/x-docs/roadmap.md @@ -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