New features
This commit is contained in:
@@ -12,9 +12,12 @@ import picocli.CommandLine.Parameters;
|
||||
@Command(name = "search-identifier", mixinStandardHelpOptions = true, description = "Search for an identifier across all modules, or list all identifiers")
|
||||
final class SearchIdentifierCommand extends AbstractProjectCommand {
|
||||
|
||||
@Parameters(index = "0", arity = "0..1", description = "Identifier name; a leading Natural sigil (# & +) is ignored (omit to list all identifiers)")
|
||||
@Parameters(index = "0", arity = "0..1", description = "Identifier name — a Java type's short or fully-qualified form; a leading Natural sigil (# & +) is ignored (omit to list all identifiers)")
|
||||
@Nullable String name;
|
||||
|
||||
@Option(names = "--contains", description = "Match names that contain the given name (case-insensitive) instead of equalling it; requires a name")
|
||||
boolean contains;
|
||||
|
||||
@Option(names = "--type", description = "Node type filter (MODULE, FUNCTION, VARIABLE, DATA_STRUCTURE, DB_TABLE)")
|
||||
@Nullable String type;
|
||||
|
||||
@@ -42,6 +45,9 @@ final class SearchIdentifierCommand extends AbstractProjectCommand {
|
||||
path = appendQuery(path, "module", module);
|
||||
path = appendQuery(path, "priorityModule", priorityModule);
|
||||
path = appendQuery(path, "sourceFile", sourceFile);
|
||||
if (contains) {
|
||||
path = appendQuery(path, "contains", "true");
|
||||
}
|
||||
path = appendQuery(appendQuery(path, "limit", limit), "offset", offset);
|
||||
return printResponse(apiClient().get(path));
|
||||
} catch (IllegalStateException e) {
|
||||
|
||||
@@ -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=215
|
||||
version=217
|
||||
|
||||
@@ -771,6 +771,7 @@ public class AnalysisResource {
|
||||
@GET
|
||||
@Path("/search/identifier")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = IdentifierMatch.class)))
|
||||
@APIResponse(responseCode = "400", description = "Unknown node 'type', or 'contains' without a 'name'.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> searchIdentifier(@PathParam("project") String project, @QueryParam("name") @Nullable String name,
|
||||
@QueryParam("type") @Nullable String type,
|
||||
@@ -780,8 +781,17 @@ public class AnalysisResource {
|
||||
@QueryParam("module") @Nullable String module,
|
||||
@Parameter(description = "Do not filter, but pin this module's matches to the front so its local declaration survives the limit when a name recurs across many modules.")
|
||||
@QueryParam("priorityModule") @Nullable String priorityModule,
|
||||
@Parameter(description = "Match names that contain 'name' (case-insensitive) instead of equalling it, as on /search/value. Matches a Java type's fully-qualified name as well as its short form, so a package fragment also hits. Requires a non-blank 'name'.")
|
||||
@QueryParam("contains") @Nullable Boolean contains,
|
||||
@QueryParam("limit") @Nullable Integer limit, @QueryParam("offset") @Nullable Integer offset,
|
||||
@QueryParam("fields") @Nullable String fields) {
|
||||
// Item 125: a substring search for nothing is a full node dump, and was previously the
|
||||
// silent outcome — the parameter did not exist, so JAX-RS dropped it and the caller read the
|
||||
// resulting [] as "no such identifier".
|
||||
if (Boolean.TRUE.equals(contains) && (name == null || name.isBlank())) {
|
||||
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "MISSING_NAME",
|
||||
"Query parameter 'name' is required when 'contains' is true"));
|
||||
}
|
||||
if (type != null) {
|
||||
try {
|
||||
NodeType.valueOf(type.toUpperCase());
|
||||
@@ -792,7 +802,8 @@ public class AnalysisResource {
|
||||
}
|
||||
return withFanoutWarm(project,
|
||||
() -> graphRepository.searchIdentifier(project, name,
|
||||
type != null ? type.toUpperCase() : null, sourceFile, module, priorityModule, effectiveLimit(limit), effectiveOffset(offset)),
|
||||
type != null ? type.toUpperCase() : null, sourceFile, module, priorityModule,
|
||||
Boolean.TRUE.equals(contains), effectiveLimit(limit), effectiveOffset(offset)),
|
||||
matches -> matches.stream().map(IdentifierMatch::sourceFile).filter(sf -> !sf.isEmpty()).distinct().toList(),
|
||||
matches -> namesOnly(fields) ? ok(identifierNames(matches)) : ok(matches));
|
||||
}
|
||||
|
||||
@@ -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 OpenAPI
|
||||
# info version (referenced below via property expression, not duplicated).
|
||||
agenticcode.version=215
|
||||
agenticcode.version=217
|
||||
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
|
||||
# is generated against. Served at /q/openapi (yaml/json); Swagger UI at /q/swagger-ui in dev.
|
||||
mp.openapi.extensions.smallrye.info.title=AgenticCode API
|
||||
|
||||
@@ -95,7 +95,7 @@ class CoalescingIT {
|
||||
assertTrue(state.isFull(), "the module is FULL after concurrent deep ingests");
|
||||
assertEquals(IngestStatus.INGESTED.name(), state.status());
|
||||
|
||||
List<IdentifierMatch> realModules = graphRepository.searchIdentifier(PROJECT, MODULE, "MODULE", null, null, null, 50, 0)
|
||||
List<IdentifierMatch> realModules = graphRepository.searchIdentifier(PROJECT, MODULE, "MODULE", null, null, null, false, 50, 0)
|
||||
.await().indefinitely().stream()
|
||||
.filter(m -> !m.sourceFile().isEmpty())
|
||||
.toList();
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* Item 125: a Java type declaration must be findable through {@code search/identifier} by its
|
||||
* <b>short</b> name, not only by the fully-qualified identity the graph stores (item 117), and
|
||||
* {@code ?contains=} must do what it does on {@code /search/value} instead of being dropped.
|
||||
*
|
||||
* <p>The bug this pins down was not "types are not indexed" — they were, under their FQN — but that
|
||||
* the short form an agent actually types answered {@code []}, which is indistinguishable from "no
|
||||
* such name". The match now also carries {@code simpleName} and {@code moduleKind}, so "is this a
|
||||
* type or a method?" is answered by the search itself.
|
||||
*/
|
||||
@QuarkusTest
|
||||
class IdentifierTypeDeclarationIT {
|
||||
|
||||
private static final String PROJECT = "identifier-type-decl-project";
|
||||
private static final String FQN = "com.example.idt.PartnerUpdateLogic";
|
||||
|
||||
@TempDir
|
||||
static Path root;
|
||||
|
||||
@BeforeAll
|
||||
static void ingestFixtures() {
|
||||
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
|
||||
Path pkg = root.resolve("src/main/java/com/example/idt");
|
||||
writeSource(pkg, "PartnerUpdateLogic.java", """
|
||||
package com.example.idt;
|
||||
|
||||
public class PartnerUpdateLogic {
|
||||
private String partnerName;
|
||||
|
||||
public void updatePartner() {
|
||||
this.partnerName = "x";
|
||||
}
|
||||
}
|
||||
""");
|
||||
writeSource(pkg, "PartnerReadPort.java", """
|
||||
package com.example.idt;
|
||||
|
||||
public interface PartnerReadPort {
|
||||
String read();
|
||||
}
|
||||
""");
|
||||
given()
|
||||
.contentType("application/json")
|
||||
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "java", 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 writeSource(Path dir, String fileName, String content) {
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve(fileName), content);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static io.restassured.specification.RequestSpecification search() {
|
||||
return given().queryParam("type", "MODULE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theShortNameFindsTheTypeDeclaration() {
|
||||
search()
|
||||
.queryParam("name", "PartnerUpdateLogic")
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("name", hasItem(FQN))
|
||||
.body("find { it.name == '" + FQN + "' }.simpleName", equalTo("PartnerUpdateLogic"))
|
||||
.body("find { it.name == '" + FQN + "' }.moduleKind", equalTo("CLASS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void theQualifiedNameStillFindsIt() {
|
||||
search()
|
||||
.queryParam("name", FQN)
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("name", hasItem(FQN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void moduleKindDistinguishesAnInterfaceFromAClass() {
|
||||
search()
|
||||
.queryParam("name", "PartnerReadPort")
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("moduleKind", everyItem(equalTo("INTERFACE")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsMatchesASubstringOfTheShortNameCaseInsensitively() {
|
||||
search()
|
||||
.queryParam("name", "partnerupd").queryParam("contains", true)
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("name", hasItem(FQN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsAlsoMatchesThePackageFragmentOfTheQualifiedName() {
|
||||
search()
|
||||
.queryParam("name", "com.example.idt").queryParam("contains", true)
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("name", hasItems(FQN, "com.example.idt.PartnerReadPort"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutContainsASubstringIsNotAMatch() {
|
||||
search()
|
||||
.queryParam("name", "PartnerUpd")
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("$", hasSize(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aMethodMatchCarriesNoSimpleNameOrModuleKind() {
|
||||
given()
|
||||
.queryParam("name", "updatePartner")
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("find { it.type == 'FUNCTION' }.simpleName", nullValue())
|
||||
.body("find { it.type == 'FUNCTION' }.moduleKind", nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsWithoutANameIsRejectedRatherThanDumpingEveryNode() {
|
||||
given()
|
||||
.queryParam("contains", true)
|
||||
.when().get("/api/projects/" + PROJECT + "/search/identifier")
|
||||
.then()
|
||||
.statusCode(400)
|
||||
.body("code", equalTo("MISSING_NAME"));
|
||||
}
|
||||
}
|
||||
@@ -2386,8 +2386,22 @@ public final class CypherQueries {
|
||||
// when a name recurs across many modules. ORDER BY makes the page deterministic (it was not before).
|
||||
public static final String SEARCH_IDENTIFIER = """
|
||||
MATCH (n:AstNode {project: $project})
|
||||
// Item 125: a Java type declaration's identity is its FQN (item 117), so matching n.name
|
||||
// alone made a class findable only by its fully-qualified name — the short form an agent
|
||||
// actually types answered [], which reads as "no such name" rather than "wrong form of the
|
||||
// name". n.simpleName is matched alongside it. It is null for Natural, where the
|
||||
// sigil-stripped n.name remains the only identity, so nothing changes there.
|
||||
WITH n, (CASE WHEN left(n.name, 1) IN ['#', '&', '+'] THEN substring(n.name, 1) ELSE n.name END) AS bare
|
||||
// $contains (item 125) switches the name predicate to a case-insensitive substring match,
|
||||
// as /search/value already does — "every identifier containing 'upd'" was unanswerable.
|
||||
// It matches the FQN too, so a package fragment also hits: over-matching is visible and
|
||||
// filterable (?type=MODULE, moduleKind), silence is not.
|
||||
WHERE ($name IS NULL OR
|
||||
(CASE WHEN left(n.name, 1) IN ['#', '&', '+'] THEN substring(n.name, 1) ELSE n.name END) = $name)
|
||||
CASE WHEN $contains
|
||||
THEN toLower(bare) CONTAINS toLower($name)
|
||||
OR (n.simpleName IS NOT NULL AND toLower(n.simpleName) CONTAINS toLower($name))
|
||||
ELSE bare = $name OR n.simpleName = $name
|
||||
END)
|
||||
AND ($type IS NULL OR n.type = $type)
|
||||
AND ($sourceFile IS NULL OR n.sourceFile = $sourceFile)
|
||||
AND ($module IS NULL OR EXISTS {
|
||||
@@ -2402,8 +2416,18 @@ public final class CypherQueries {
|
||||
} THEN 0 ELSE 1 END AS pinRank
|
||||
RETURN elementId(n) AS id, n.type AS type, n.name AS name, n.sourceFile AS sourceFile,
|
||||
n.startLine AS startLine, n.endLine AS endLine,
|
||||
n.dataType AS dataType, n.value AS value, n.scope AS scope, n.unresolved AS unresolved
|
||||
n.dataType AS dataType, n.value AS value, n.scope AS scope, n.unresolved AS unresolved,
|
||||
// Item 125: the short form and the declaration kind (CLASS/INTERFACE/ENUM/RECORD,
|
||||
// PROGRAM/SUBPROGRAM for Natural), so a caller can tell a type declaration from a
|
||||
// method or a variable without a second round trip. Null for non-MODULE nodes.
|
||||
n.simpleName AS simpleName, n.moduleKind AS moduleKind
|
||||
ORDER BY pinRank ASC, n.sourceFile ASC, n.startLine ASC
|
||||
// $scanCap bounds an unindexed CONTAINS over a large project: it is offset+limit, i.e.
|
||||
// exactly the rows the caller's page can need, and Integer.MAX_VALUE when the caller asked
|
||||
// for everything (limit <= 0). Slicing [offset, offset+limit) out of the first offset+limit
|
||||
// ordered rows is identical to slicing it out of the full ordered list, so paginate()'s
|
||||
// semantics — including "limit <= 0 means unlimited" — are untouched.
|
||||
LIMIT $scanCap
|
||||
""";
|
||||
/**
|
||||
* Item 78: deletes a project's {@code AstNode}s <b>in batches</b>, leaving its {@code (:Project)}
|
||||
|
||||
@@ -1081,19 +1081,37 @@ public class GraphRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated variant of {@link #searchIdentifier(String, String, String, String, String, String)} for the endpoint.
|
||||
* Paginated variant of {@link #searchIdentifier(String, String, String, String, String, String, boolean, int)}
|
||||
* for the endpoint.
|
||||
*
|
||||
* @param contains item 125: match names that <em>contain</em> {@code identifierName}
|
||||
* (case-insensitive) instead of equalling it
|
||||
*/
|
||||
public Uni<List<IdentifierMatch>> searchIdentifier(String project, @Nullable String identifierName,
|
||||
@Nullable String type, @Nullable String sourceFile,
|
||||
@Nullable String module, @Nullable String priorityModule,
|
||||
int limit, int offset) {
|
||||
return searchIdentifier(project, identifierName, type, sourceFile, module, priorityModule)
|
||||
boolean contains, int limit, int offset) {
|
||||
// Item 125: fetch at most the rows this page can consume, so an unindexed CONTAINS over a
|
||||
// large project cannot materialise its whole match set here. paginate() then slices exactly
|
||||
// as before — including its "limit <= 0 means unlimited" rule, which is why the cap is
|
||||
// Integer.MAX_VALUE in that case rather than 0.
|
||||
int scanCap = limit > 0 ? (int) Math.min((long) Math.max(offset, 0) + limit, Integer.MAX_VALUE)
|
||||
: Integer.MAX_VALUE;
|
||||
return searchIdentifier(project, identifierName, type, sourceFile, module, priorityModule, contains, scanCap)
|
||||
.map(list -> paginate(list, limit, offset));
|
||||
}
|
||||
|
||||
public Uni<List<IdentifierMatch>> searchIdentifier(String project, @Nullable String identifierName,
|
||||
@Nullable String type, @Nullable String sourceFile,
|
||||
@Nullable String module, @Nullable String priorityModule) {
|
||||
return searchIdentifier(project, identifierName, type, sourceFile, module, priorityModule,
|
||||
false, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
public Uni<List<IdentifierMatch>> searchIdentifier(String project, @Nullable String identifierName,
|
||||
@Nullable String type, @Nullable String sourceFile,
|
||||
@Nullable String module, @Nullable String priorityModule,
|
||||
boolean contains, int scanCap) {
|
||||
Map<String, @Nullable Object> params = new java.util.HashMap<>();
|
||||
params.put("project", project);
|
||||
// Sigil-insensitive: strip a leading Natural sigil (# user, & AIV, + GDA) so a search for
|
||||
@@ -1106,6 +1124,9 @@ public class GraphRepository {
|
||||
// Pin this module's matches to the front so they survive limit/paginate truncation when a
|
||||
// name recurs across many modules (the caller's local declaration would otherwise be lost).
|
||||
params.put("priorityModule", priorityModule);
|
||||
// Item 125: substring vs exact name match, and the row cap the query's LIMIT reads.
|
||||
params.put("contains", contains);
|
||||
params.put("scanCap", scanCap);
|
||||
return read(CypherQueries.SEARCH_IDENTIFIER, params, record -> new IdentifierMatch(
|
||||
record.get("id").asString(),
|
||||
NodeType.valueOf(record.get("type").asString()),
|
||||
@@ -1116,7 +1137,9 @@ public class GraphRepository {
|
||||
record.get("dataType").isNull() ? null : record.get("dataType").asString(),
|
||||
record.get("value").isNull() ? null : record.get("value").asString(),
|
||||
record.get("scope").isNull() ? null : record.get("scope").asString(),
|
||||
record.get("unresolved").isNull() ? null : record.get("unresolved").asBoolean()));
|
||||
record.get("unresolved").isNull() ? null : record.get("unresolved").asBoolean(),
|
||||
record.get("simpleName").isNull() ? null : record.get("simpleName").asString(),
|
||||
record.get("moduleKind").isNull() ? null : record.get("moduleKind").asString()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,8 +13,16 @@ import org.jspecify.annotations.Nullable;
|
||||
* @param unresolved item 40: {@code true} when this match is an unresolved reference placeholder
|
||||
* ({@code sourceFile} is blank and no real definition of the name is ingested);
|
||||
* {@code false} for a resolved placeholder, {@code null} for a normal (real) node
|
||||
* @param simpleName item 125: a Java type declaration's short form, where {@code name} is its
|
||||
* fully-qualified identity (item 117). {@code null} for every non-{@code MODULE} node and
|
||||
* for Natural modules, whose {@code name} already is the short form
|
||||
* @param moduleKind item 125: the declaration kind of a {@code MODULE} match — {@code CLASS},
|
||||
* {@code INTERFACE}, {@code ENUM}, {@code RECORD} for Java, {@code PROGRAM},
|
||||
* {@code SUBPROGRAM} for Natural — so "is this name a type or a method?" is answered by
|
||||
* the search itself rather than by a follow-up call. {@code null} for non-{@code MODULE} nodes
|
||||
*/
|
||||
public record IdentifierMatch(String id, NodeType type, String name, String sourceFile, int startLine, int endLine,
|
||||
@Nullable String dataType, @Nullable String value, @Nullable String scope,
|
||||
@Nullable Boolean unresolved) {
|
||||
@Nullable Boolean unresolved, @Nullable String simpleName,
|
||||
@Nullable String moduleKind) {
|
||||
}
|
||||
|
||||
@@ -673,31 +673,31 @@ origins (`http://localhost:5173`, `http://localhost:4173`) — extend the
|
||||
|
||||
## Endpoint quick reference
|
||||
|
||||
| Endpoint | Use for |
|
||||
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GET /modules?sourceFile=&moduleKind=&extends=` | List/filter modules; map a source file to its module name(s). Each row carries `loc`/`sloc` (item 46) and `ingestStatus`/`ingestDepth` (item 50) for status badges without a per-module round trip |
|
||||
| `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`. `callers` `scope`: **`external` (default)** = modules that call this one (CALLNAT/inheritance), **rolled up to the calling MODULE**: a call made from inside a subroutine/method is attributed to its owning module (never the calling `FUNCTION` node), and repeated call sites from one caller collapse to a single row whose `sites` list every line — symmetric with how `callees` anchors its source side. `internal` = the module's own subroutines' `PERFORM` wiring (function-level). The default is external-only, module-typed 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 / function-level 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. CLI `ac function-callers <module> <function>` |
|
||||
| `GET /modules/{name}/call-tree?depth=` | Transitive call graph to scope a feature
|
||||
| `GET /modules/{name}/reaches?target=A,B,C&direction=up\|down&depth=` | **Item 110 — "can A reach B, and how?"** Returns `{reachable, paths, truncated}` with one witness route per reached target (module names, source→target). `direction=down` (default): paths from this module to each target. `up`: paths from each target to this module. The counterpart to `call-tree`, which only walks downward and returns a closure without routes — one audit hand-rolled this as ~100 `/callers` requests. **`reachable: false` means "no path over known edges", not "no path"**: the traversal runs on resolved module calls, so a route through an unresolved dynamic `CALLNAT` (item 82) is invisible. Bounded by `depth` (item 75: the call graph has cycles). CLI `ac reaches <module> --target A,B --direction up` |
|
||||
| `GET /duplicates` | **Item 114 — identities skipped at ingest** because they exist in more than one file (`{name, kind, paths}`, paths relative to the project root). These are *not* in `/modules`; asking for one by name gives `409 DUPLICATE_IDENTITY`. Their own calls are absent from the graph, so caller lists elsewhere can be short. CLI `ac duplicates` | |
|
||||
| `GET /dynamic-calls/unresolved` \| `/overrides` · `POST`/`DELETE /overrides` | **Manual dynamic-`CALLNAT` overrides (item 82).** `unresolved` lists open `CALLNAT <var>` sites `{module, originFile, lineNo, variable}`; `POST /overrides {originFile, lineNo, targets[], variable?, note?}` pins a site to real module(s) (applied at once, persisted across refreshes, `400 UNKNOWN_TARGET` for a non-module); `DELETE /overrides?originFile=&lineNo=` resets one site (omit both = all) and restores the placeholder inline; `GET /overrides` lists them with an `obsolete` flag. CLI `ac dynamic-calls unresolved\|overrides\|set\|reset` |
|
||||
| `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`. CLI `ac ego-graph` |
|
||||
| `GET /modules/{name}/db-accesses` \| `/sql-statements` | DB tables + mode, raw statement text (pass `?depth=` for Natural). **`db-accesses`/`workfile-accesses` return every row when no `limit` is given (item 103)** — they used to default to 50, and since the response is a bare array with no total and no `truncated` flag the cut was invisible: `WGEAGB0S?depth=10` returned 50 of 64 rows and hid 7 tables outright. An explicit `limit` is still honoured exactly. `db-accesses` items carry **`sites: [{lineNo, sourceFile, viaCopycode, includedAt}]`** (+ kept `lineNos`); `sql-statements` items carry **`sourceFile`** + **`viaCopycode`** — so a copycode-sourced access (e.g. `SELECT … FROM SYSIBM-SYSDUMMY1` in `USIX043C.cpy`) reports the `.cpy` line, not a bare number that reads as a host-file line |
|
||||
| `GET /modules/{name}/workfile-accesses` | Natural **work files** (sequential/flat-file I/O — `READ`/`WRITE WORK FILE n`), the work-file analogue of `db-accesses` (item 84): `[{workFile, physicalName, mode: READS\|WRITES, recordBuffers, lineNos, sites}]`, aggregated per work-file number + mode. `sites: [{lineNo, sourceFile, viaCopycode, includedAt}]` gives each access its file context (copycode-aware), like `db-accesses`. `physicalName` comes from a `DEFINE WORK FILE n '<name>'`, else `null`. **Kept separate from `db-accesses`** — a work file is not an ADABAS/SQL table (fixes a former bug where `READ WORK FILE` created a phantom `DB_TABLE 'WORK'`). CLI `ac workfile-accesses <module>` |
|
||||
| `GET /modules/{name}/data-structures` | Which copybooks/inline groups a module uses. A `USING <member>` binds by **member (file) name**, never by a level-1 record inside the file (item 100) — before that, `WGEAGB0S USING W-WIF-A2` reported `old/W-WIF-A7.pda` (whose level-1 record is a copy-pasted `1W-WIF-A2`), and a data area with several level-1 records and none named after the member (`VLAYERLA.lda`, `USIX020L.lda`) resolved to nothing at all (`sourceFile: null`, `area: UNKNOWN`, `fieldCount: 0`) although the file was ingested. One row per resolved definition, `(name, sourceFile)` (item 102) — never one row blending an arbitrary file with another definition's `fieldCount` |
|
||||
| `GET /modules/{name}/payload` | Natural XML wire-payload contract: `{tag, field, direction, source, lineNo, sourceFile}` — static `ADD-XML-LINE` idiom (`source=IDIOM`, item 45) or derived from the wrapper's interface PDA (`source=PDA`, item 46b). `sourceFile` is the file `lineNo` refers to (module for IDIOM, PDA for PDA) |
|
||||
| `GET /modules/{name}/dispatch-table` | Natural `DECIDE ON VALUE OF` routing table |
|
||||
| `GET /modules/{name}/functions?kind=` \| `/functions/{fn}/overrides` \| `/functions/overrides` | Method list, modifier filter (Java), subclass overrides (single/bulk). Each item carries **`sourceFile`** + **`viaCopycode`** (item 84): a Natural subroutine pulled in via `INCLUDE` reports the **copycode** file and `viaCopycode:true`, so its `startLine`/`endLine` are read as offsets into that copycode — **not** into the including module's own file (which is shorter). `viaCopycode:false` = declared inline. Always `false` for Java |
|
||||
| `GET /data-structures/{name}/fields` \| `/db-tables/{name}/columns` \| `/modules/{name}/columns` | Field/column schemas for DTO/entity generation. Every field carries **`sourceFile`** (item 101). When a structure name resolves to several definitions (42 level-1 names recur across `upms` data areas), the **member root** — the definition whose file basename equals the name, i.e. what a `USING <member>` binds to — wins; **`?sourceFile=`** pins a specific one. Before item 101 the definitions were silently unioned: `W-WIF-A2` returned 15 fields, the merge of `W-WIF-A2.pda` (5) and `W-WIF-A7.pda` (10), a layout that exists nowhere |
|
||||
| `GET /variables/{name}/reads` \| `/writes` \| `/flow-forward` \| `/flow-backward` \| `/field-flow` | Impact analysis and dataflow tracing |
|
||||
| `GET /search/identifier` \| `/search/value` \| `/search/annotation` | Cross-project lookup by name / literal value / annotation. `search/identifier` matches the **exact** declared name but is **sigil-insensitive**: a leading Natural sigil (`#` user, `&` AIV, `+` GDA) is ignored on both sides, so `name=K-OUT-MAX` finds the declared `#K-OUT-MAX` (and vice-versa). Optional **scope** filters `sourceFile=<relpath>` and `module=<name>` (item 53) narrow the match to one file / one module — use them to pinpoint a module-local declaration when a name recurs across dozens of modules (the result is otherwise paginated and the local one may fall off the page). To keep the **full cross-project list** yet still guarantee a given module's own declaration is on the first page, pass `priorityModule=<name>` instead of `module=`: it does not filter, but pins that module's matches to the front (ahead of the otherwise `sourceFile`-ordered rest) so they survive the `limit`. This is what the web UI's click-to-identify sends for the open module. CLI `ac search-identifier --module --priority-module --source-file --type` accept the same filters. **Latency (item 105):** a lookup whose hits lie in a Natural data area used to take 60-75 s — every fan-out query deep-ingested the surfaced `.lda`/`.pda`, which can never reach `FULL` (a data area yields no `MODULE` node), so it was re-warmed on every call and each warm dragged a whole-project finalize behind it. Data areas are now excluded from the fan-out warm; they have no deep tier to gain |
|
||||
| `GET /search/source?regex=&limit=&ignoreCase=` (`ac search-source`) | Regex **grep over module source text** (item 54): `{module, sourceFile, lineNo, line}` hits + `truncated`. Case-insensitive by default. Complements `/search/identifier` (declared names) — use for code patterns (statements, table names, literals) |
|
||||
| `GET /nodes/{id}` | Every property of one node (when a curated DTO is missing something) |
|
||||
| `GET /nodes/{id}/source` \| `/modules/{name}/source` \| `/source?file=` | Source text — **only when you have no other access to the source** (you always do in this repo, see "Reading source in this repo" above). `/modules/{name}/source` returns the **whole file** when the line range is omitted (M1), or a `[startLine,endLine]` slice when both are given. `/source?file=<relpath>` (CLI `ac file-source`) serves a file by **relative path** rather than module name — for files that aren't standalone modules, e.g. a Natural data area (PDA/LDA) USING'd by a module, whose field line numbers refer to that file. Same whole-file/range + stale-source semantics; the client-supplied path is rejected (`400 INVALID_SOURCE_FILE`) if it escapes the project root |
|
||||
| Endpoint | Use for |
|
||||
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GET /modules?sourceFile=&moduleKind=&extends=` | List/filter modules; map a source file to its module name(s). Each row carries `loc`/`sloc` (item 46) and `ingestStatus`/`ingestDepth` (item 50) for status badges without a per-module round trip |
|
||||
| `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`. `callers` `scope`: **`external` (default)** = modules that call this one (CALLNAT/inheritance), **rolled up to the calling MODULE**: a call made from inside a subroutine/method is attributed to its owning module (never the calling `FUNCTION` node), and repeated call sites from one caller collapse to a single row whose `sites` list every line — symmetric with how `callees` anchors its source side. `internal` = the module's own subroutines' `PERFORM` wiring (function-level). The default is external-only, module-typed 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 / function-level 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. CLI `ac function-callers <module> <function>` |
|
||||
| `GET /modules/{name}/call-tree?depth=` | Transitive call graph to scope a feature
|
||||
| `GET /modules/{name}/reaches?target=A,B,C&direction=up\|down&depth=` | **Item 110 — "can A reach B, and how?"** Returns `{reachable, paths, truncated}` with one witness route per reached target (module names, source→target). `direction=down` (default): paths from this module to each target. `up`: paths from each target to this module. The counterpart to `call-tree`, which only walks downward and returns a closure without routes — one audit hand-rolled this as ~100 `/callers` requests. **`reachable: false` means "no path over known edges", not "no path"**: the traversal runs on resolved module calls, so a route through an unresolved dynamic `CALLNAT` (item 82) is invisible. Bounded by `depth` (item 75: the call graph has cycles). CLI `ac reaches <module> --target A,B --direction up` |
|
||||
| `GET /duplicates` | **Item 114 — identities skipped at ingest** because they exist in more than one file (`{name, kind, paths}`, paths relative to the project root). These are *not* in `/modules`; asking for one by name gives `409 DUPLICATE_IDENTITY`. Their own calls are absent from the graph, so caller lists elsewhere can be short. CLI `ac duplicates` | |
|
||||
| `GET /dynamic-calls/unresolved` \| `/overrides` · `POST`/`DELETE /overrides` | **Manual dynamic-`CALLNAT` overrides (item 82).** `unresolved` lists open `CALLNAT <var>` sites `{module, originFile, lineNo, variable}`; `POST /overrides {originFile, lineNo, targets[], variable?, note?}` pins a site to real module(s) (applied at once, persisted across refreshes, `400 UNKNOWN_TARGET` for a non-module); `DELETE /overrides?originFile=&lineNo=` resets one site (omit both = all) and restores the placeholder inline; `GET /overrides` lists them with an `obsolete` flag. CLI `ac dynamic-calls unresolved\|overrides\|set\|reset` |
|
||||
| `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`. CLI `ac ego-graph` |
|
||||
| `GET /modules/{name}/db-accesses` \| `/sql-statements` | DB tables + mode, raw statement text (pass `?depth=` for Natural). **`db-accesses`/`workfile-accesses` return every row when no `limit` is given (item 103)** — they used to default to 50, and since the response is a bare array with no total and no `truncated` flag the cut was invisible: `WGEAGB0S?depth=10` returned 50 of 64 rows and hid 7 tables outright. An explicit `limit` is still honoured exactly. `db-accesses` items carry **`sites: [{lineNo, sourceFile, viaCopycode, includedAt}]`** (+ kept `lineNos`); `sql-statements` items carry **`sourceFile`** + **`viaCopycode`** — so a copycode-sourced access (e.g. `SELECT … FROM SYSIBM-SYSDUMMY1` in `USIX043C.cpy`) reports the `.cpy` line, not a bare number that reads as a host-file line |
|
||||
| `GET /modules/{name}/workfile-accesses` | Natural **work files** (sequential/flat-file I/O — `READ`/`WRITE WORK FILE n`), the work-file analogue of `db-accesses` (item 84): `[{workFile, physicalName, mode: READS\|WRITES, recordBuffers, lineNos, sites}]`, aggregated per work-file number + mode. `sites: [{lineNo, sourceFile, viaCopycode, includedAt}]` gives each access its file context (copycode-aware), like `db-accesses`. `physicalName` comes from a `DEFINE WORK FILE n '<name>'`, else `null`. **Kept separate from `db-accesses`** — a work file is not an ADABAS/SQL table (fixes a former bug where `READ WORK FILE` created a phantom `DB_TABLE 'WORK'`). CLI `ac workfile-accesses <module>` |
|
||||
| `GET /modules/{name}/data-structures` | Which copybooks/inline groups a module uses. A `USING <member>` binds by **member (file) name**, never by a level-1 record inside the file (item 100) — before that, `WGEAGB0S USING W-WIF-A2` reported `old/W-WIF-A7.pda` (whose level-1 record is a copy-pasted `1W-WIF-A2`), and a data area with several level-1 records and none named after the member (`VLAYERLA.lda`, `USIX020L.lda`) resolved to nothing at all (`sourceFile: null`, `area: UNKNOWN`, `fieldCount: 0`) although the file was ingested. One row per resolved definition, `(name, sourceFile)` (item 102) — never one row blending an arbitrary file with another definition's `fieldCount` |
|
||||
| `GET /modules/{name}/payload` | Natural XML wire-payload contract: `{tag, field, direction, source, lineNo, sourceFile}` — static `ADD-XML-LINE` idiom (`source=IDIOM`, item 45) or derived from the wrapper's interface PDA (`source=PDA`, item 46b). `sourceFile` is the file `lineNo` refers to (module for IDIOM, PDA for PDA) |
|
||||
| `GET /modules/{name}/dispatch-table` | Natural `DECIDE ON VALUE OF` routing table |
|
||||
| `GET /modules/{name}/functions?kind=` \| `/functions/{fn}/overrides` \| `/functions/overrides` | Method list, modifier filter (Java), subclass overrides (single/bulk). Each item carries **`sourceFile`** + **`viaCopycode`** (item 84): a Natural subroutine pulled in via `INCLUDE` reports the **copycode** file and `viaCopycode:true`, so its `startLine`/`endLine` are read as offsets into that copycode — **not** into the including module's own file (which is shorter). `viaCopycode:false` = declared inline. Always `false` for Java |
|
||||
| `GET /data-structures/{name}/fields` \| `/db-tables/{name}/columns` \| `/modules/{name}/columns` | Field/column schemas for DTO/entity generation. Every field carries **`sourceFile`** (item 101). When a structure name resolves to several definitions (42 level-1 names recur across `upms` data areas), the **member root** — the definition whose file basename equals the name, i.e. what a `USING <member>` binds to — wins; **`?sourceFile=`** pins a specific one. Before item 101 the definitions were silently unioned: `W-WIF-A2` returned 15 fields, the merge of `W-WIF-A2.pda` (5) and `W-WIF-A7.pda` (10), a layout that exists nowhere |
|
||||
| `GET /variables/{name}/reads` \| `/writes` \| `/flow-forward` \| `/flow-backward` \| `/field-flow` | Impact analysis and dataflow tracing |
|
||||
| `GET /search/identifier` \| `/search/value` \| `/search/annotation` | Cross-project lookup by name / literal value / annotation. `search/identifier` matches the **exact** declared name but is **sigil-insensitive**: a leading Natural sigil (`#` user, `&` AIV, `+` GDA) is ignored on both sides, so `name=K-OUT-MAX` finds the declared `#K-OUT-MAX` (and vice-versa). **Item 125:** a Java **type declaration** is matched by its **short name** as well as by the fully-qualified identity the graph stores (item 117) — `name=PartnerUpdateLogic` finds `com.example.PartnerUpdateLogic`; before this it answered `[]`, which reads as "no such name". Every match carries `simpleName` and `moduleKind` (`CLASS`/`INTERFACE`/`ENUM`/`RECORD`, `PROGRAM`/`SUBPROGRAM` for Natural), both `null` for non-`MODULE` hits — so "is this name a type or a method?" needs no second call. `contains=true` (CLI `--contains`) switches to a case-insensitive **substring** match, as on `/search/value`; it was previously accepted and silently dropped. It matches the FQN too, so a package fragment also hits — filter with `type=MODULE`/`moduleKind` if that is noise. `contains` without a `name` is `400 MISSING_NAME` (a substring search for nothing is a full node dump). The substring scan is **unindexed**: it is bounded to `offset+limit` rows, so keep a `limit` on large projects. Optional **scope** filters `sourceFile=<relpath>` and `module=<name>` (item 53) narrow the match to one file / one module — use them to pinpoint a module-local declaration when a name recurs across dozens of modules (the result is otherwise paginated and the local one may fall off the page). To keep the **full cross-project list** yet still guarantee a given module's own declaration is on the first page, pass `priorityModule=<name>` instead of `module=`: it does not filter, but pins that module's matches to the front (ahead of the otherwise `sourceFile`-ordered rest) so they survive the `limit`. This is what the web UI's click-to-identify sends for the open module. CLI `ac search-identifier --module --priority-module --source-file --type --contains` accept the same filters. **Latency (item 105):** a lookup whose hits lie in a Natural data area used to take 60-75 s — every fan-out query deep-ingested the surfaced `.lda`/`.pda`, which can never reach `FULL` (a data area yields no `MODULE` node), so it was re-warmed on every call and each warm dragged a whole-project finalize behind it. Data areas are now excluded from the fan-out warm; they have no deep tier to gain |
|
||||
| `GET /search/source?regex=&limit=&ignoreCase=` (`ac search-source`) | Regex **grep over module source text** (item 54): `{module, sourceFile, lineNo, line}` hits + `truncated`. Case-insensitive by default. Complements `/search/identifier` (declared names) — use for code patterns (statements, table names, literals) |
|
||||
| `GET /nodes/{id}` | Every property of one node (when a curated DTO is missing something) |
|
||||
| `GET /nodes/{id}/source` \| `/modules/{name}/source` \| `/source?file=` | Source text — **only when you have no other access to the source** (you always do in this repo, see "Reading source in this repo" above). `/modules/{name}/source` returns the **whole file** when the line range is omitted (M1), or a `[startLine,endLine]` slice when both are given. `/source?file=<relpath>` (CLI `ac file-source`) serves a file by **relative path** rather than module name — for files that aren't standalone modules, e.g. a Natural data area (PDA/LDA) USING'd by a module, whose field line numbers refer to that file. Same whole-file/range + stale-source semantics; the client-supplied path is rejected (`400 INVALID_SOURCE_FILE`) if it escapes the project root |
|
||||
|
||||
Full endpoint list, request params, and response field details:
|
||||
`x-docs/agent-api-system-prompt.md`.
|
||||
|
||||
@@ -38,11 +38,15 @@ tracks only items that are still open.
|
||||
|
||||
## High priority — Agent API gaps found in the UPMS→PUR reengineering (2026-08-18)
|
||||
|
||||
Six gaps hit during a week of daily agent use on `upms` / `pur` / `app`, ordered by the time they
|
||||
Six gaps reported after a week of daily agent use on `upms` / `pur` / `app`, ordered by the time they
|
||||
cost. All probes below were run against a freshly refreshed graph and are reproducible as written.
|
||||
Items 1 and 3 make the API return a *wrong* answer rather than a missing one, which is why they lead.
|
||||
Items 125 and 127 were the two said to make the API return a *wrong* answer rather than a missing
|
||||
one, which is why they led the list. **125 is fixed (2026-08-18); 127 was retracted the same day —
|
||||
its probe was not ambiguous, so the answer had been correct all along (see the retraction below).**
|
||||
That leaves **126, 128, 129, 130** open.
|
||||
|
||||
- [ ] **125. `search/identifier` does not index type declarations — and silently ignores `contains`**
|
||||
- [x] **125. `search/identifier` did not match a type declaration's short name — and silently ignored `contains`** (
|
||||
fixed 2026-08-18)
|
||||
|
||||
**Symptom.** The class is in the graph, but the search endpoint cannot find it.
|
||||
```
|
||||
@@ -60,10 +64,22 @@ Items 1 and 3 make the API return a *wrong* answer rather than a missing one, wh
|
||||
rather than a match or an error. "Every identifier containing `upd`" was the central question of
|
||||
a rename pass and could not be answered at all.
|
||||
|
||||
**Fix.** Index type declarations (class, interface, enum, record, and Natural data structures)
|
||||
in the same identifier index, with a `type` discriminator so callers can filter. Honour
|
||||
`contains` here as it is honoured on `/search/value`, or reject it with 400 rather than
|
||||
returning an empty result that reads like "no such name".
|
||||
**Diagnosis (2026-08-18).** The reported cause was wrong in a way worth recording: type
|
||||
declarations **are** indexed — `?name=com.agenticcode.neo4jstore.graph.GraphRepository` returns the
|
||||
`MODULE` node. `SEARCH_IDENTIFIER` matched `n.name` alone, and a Java module's `name` is its FQN
|
||||
(item 117) while the short form lives in `n.simpleName`. So the index was complete and only the
|
||||
*short* key was missing — which produced exactly the reported symptom. `contains` was not a
|
||||
parameter of the endpoint at all, so JAX-RS dropped it silently.
|
||||
|
||||
**Delivered.** `SEARCH_IDENTIFIER` matches the sigil-stripped `n.name` **or** `n.simpleName`;
|
||||
`?contains=true` (CLI `ac search-identifier --contains`) switches both to a case-insensitive
|
||||
substring match as on `/search/value`, and `contains` without a `name` is `400 MISSING_NAME`
|
||||
rather than a full node dump. Each `IdentifierMatch` now carries `simpleName` and `moduleKind`
|
||||
(`CLASS`/`INTERFACE`/`ENUM`/`RECORD`, `PROGRAM`/`SUBPROGRAM` for Natural, `null` for non-`MODULE`
|
||||
hits) — the discriminator the item asked for. The unindexed substring scan is bounded to
|
||||
`offset+limit` rows via a `$scanCap` on the query's `LIMIT`, chosen so `paginate()`'s existing
|
||||
"`limit <= 0` means unlimited" rule is untouched. Covered by `IdentifierTypeDeclarationIT`
|
||||
(8 tests); `x-docs/agent-api-usage-ac-implementation.md` updated.
|
||||
|
||||
- [ ] **126. `/projects` carries no ingest metadata, so "not found" is never evidence**
|
||||
|
||||
@@ -82,21 +98,18 @@ Items 1 and 3 make the API return a *wrong* answer rather than a missing one, wh
|
||||
language/parser version per project. Ideally every response carries the project's `ingestedAt`,
|
||||
so a stale answer is visible at the point of use rather than only on the project listing.
|
||||
|
||||
- [ ] **127. An ambiguous simple module name is resolved silently to one candidate**
|
||||
- *Not a bug — retracted 2026-08-18 (was #127): "an ambiguous simple module name is resolved
|
||||
silently to one candidate".* The probe picked a name that is **not** ambiguous: `pur` holds exactly
|
||||
**one** module with `simpleName = Builder` (`…KeyBasedIterator.Builder`) out of 5,119, so the
|
||||
`200` was the correct answer. Item 115's refusal works and was verified on names that really do
|
||||
collide — `GET /pur/modules/AuthorizationInterceptor/context` and `/pur/modules/VoidConverter/context`
|
||||
both answer `409 AMBIGUOUS_NAME` with `candidates` **and** `qualifiedNames` in `details`. The guard
|
||||
is `withModule`/`withIngestedModule`, which reject on `state.ambiguous()`; `MODULE_INGEST_STATE`
|
||||
collects every real candidate by `name` **or** `simpleName`, so nested classes are covered.
|
||||
|
||||
**Symptom.**
|
||||
```
|
||||
GET /pur/modules/Builder/context → 200, com.uniqagroup.pur.common.batch.base.reader
|
||||
.key_based.KeyBasedIterator.Builder
|
||||
```
|
||||
Expected `409 AMBIGUOUS_NAME` (which the API guide documents as ordinary for nested classes in
|
||||
`pur`). Instead one candidate is served with no indication that others exist. Whether `pur` in
|
||||
fact holds more than one `Builder` could not be checked from the API — item 125 removes the only
|
||||
endpoint that would list them, which is part of the point.
|
||||
|
||||
**Fix.** Either raise `409` with the candidate FQNs in `details`, or answer `200` with a
|
||||
`candidates` array alongside the resolved module. A silently chosen match is the failure mode
|
||||
that is hardest to notice downstream, because the response looks entirely normal.
|
||||
The item's one genuine complaint — *"whether `pur` in fact holds more than one `Builder` could not
|
||||
be checked from the API"* — was real, and is what **item 125** now answers:
|
||||
`GET /pur/search/identifier?name=Builder&type=MODULE` lists every module sharing a short name.
|
||||
|
||||
- [ ] **128. No endpoint for "every reference site of this symbol"**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user