Return 409 NOT_INGESTED
This commit is contained in:
@@ -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=151
|
||||
version=156
|
||||
|
||||
@@ -114,8 +114,8 @@ public class AnalysisResource {
|
||||
}
|
||||
|
||||
private static Response deepIngestRequired(String project, String module, ModuleIngestState state) {
|
||||
String status = state.exists() ? "NOT_DEEPLY_INGESTED" : "NOT_INGESTED";
|
||||
String detail = state.exists()
|
||||
String status = state.ingested() ? "NOT_DEEPLY_INGESTED" : "NOT_INGESTED";
|
||||
String detail = state.ingested()
|
||||
? "Module '" + module + "' has only its call graph ingested; field-level dataflow requires a deep ingest."
|
||||
: "Module '" + module + "' is not ingested in project '" + project + "'. Ingest the call graph and/or deep-ingest it.";
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
@@ -195,32 +195,23 @@ public class AnalysisResource {
|
||||
return offset != null ? Math.max(offset, 0) : 0;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/sql-statements")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SqlStatement.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> sqlStatements(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("depth") @Nullable Integer depth) {
|
||||
return withProject(project, () -> {
|
||||
if (depth != null && depth > 0) {
|
||||
return graphRepository.sqlStatementsTransitive(project, name, depth).map(this::ok);
|
||||
}
|
||||
return graphRepository.sqlStatements(project, name).map(this::ok);
|
||||
});
|
||||
private static Response moduleNotFound(String project, String name) {
|
||||
return ProjectResource.error(Response.Status.NOT_FOUND, "MODULE_NOT_FOUND",
|
||||
"No module '" + name + "' in project '" + project + "'");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/context")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ModuleContext.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> moduleContext(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("include") @Nullable String include,
|
||||
@QueryParam("limit") @Nullable Integer limit,
|
||||
@QueryParam("offset") @Nullable Integer offset) {
|
||||
int effectiveLimit = limit != null ? Math.max(limit, 0) : 0;
|
||||
int effectiveOffset = offset != null ? Math.max(offset, 0) : 0;
|
||||
return withProject(project, () -> graphRepository.moduleContext(project, name, include, effectiveLimit, effectiveOffset)
|
||||
.map(context -> Response.ok(context).build()));
|
||||
/**
|
||||
* Item 107: the placeholder answer — the module is known to the graph but its source was never
|
||||
* ingested, so any result would be empty for want of data, not for want of matches.
|
||||
*/
|
||||
private static Response notIngested(String project, String name) {
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new DeepIngestRequired("NOT_INGESTED", name,
|
||||
"Module '" + name + "' is referenced by project '" + project + "' but its source is not "
|
||||
+ "ingested — any result here would be empty because nothing was analysed, not "
|
||||
+ "because nothing was found. Use /callers to see who references it.",
|
||||
new NextAction("POST", "/api/projects/" + project + "/ingest/" + name)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static List<NameRef> callRefNames(CallRefResponse resp) {
|
||||
@@ -406,47 +397,82 @@ public class AnalysisResource {
|
||||
return withProject(project, () -> graphRepository.dbTableColumns(project, name).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/sql-statements")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SqlStatement.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> sqlStatements(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("depth") @Nullable Integer depth) {
|
||||
return withIngestedModule(project, name, () -> {
|
||||
if (depth != null && depth > 0) {
|
||||
return graphRepository.sqlStatementsTransitive(project, name, depth).map(this::ok);
|
||||
}
|
||||
return graphRepository.sqlStatements(project, name).map(this::ok);
|
||||
});
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/context")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ModuleContext.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> moduleContext(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("include") @Nullable String include,
|
||||
@QueryParam("limit") @Nullable Integer limit,
|
||||
@QueryParam("offset") @Nullable Integer offset) {
|
||||
int effectiveLimit = limit != null ? Math.max(limit, 0) : 0;
|
||||
int effectiveOffset = offset != null ? Math.max(offset, 0) : 0;
|
||||
return withIngestedModule(project, name, () -> graphRepository.moduleContext(project, name, include, effectiveLimit, effectiveOffset)
|
||||
.map(context -> Response.ok(context).build()));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/columns")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = EntityColumn.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> entityColumns(@PathParam("project") String project, @PathParam("name") String name) {
|
||||
return withProject(project, () -> graphRepository.entityColumns(project, name).map(this::ok));
|
||||
return withIngestedModule(project, name, () -> graphRepository.entityColumns(project, name).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/data-structures")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = ModuleDataStructure.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> moduleDataStructures(@PathParam("project") String project, @PathParam("name") String name) {
|
||||
return withProject(project, () -> graphRepository.moduleDataStructures(project, name).map(this::ok));
|
||||
return withIngestedModule(project, name, () -> graphRepository.moduleDataStructures(project, name).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/payload")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = PayloadField.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> payload(@PathParam("project") String project, @PathParam("name") String name) {
|
||||
return withProject(project, () -> graphRepository.payload(project, name).map(this::ok));
|
||||
return withIngestedModule(project, name, () -> graphRepository.payload(project, name).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/dispatch-table")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = DispatchEntry.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> dispatchTable(@PathParam("project") String project, @PathParam("name") String name) {
|
||||
return withProject(project, () -> graphRepository.dispatchTable(project, name).map(this::ok));
|
||||
return withIngestedModule(project, name, () -> graphRepository.dispatchTable(project, name).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/functions")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = InheritedFunction.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> moduleFunctions(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("includeInherited") @Nullable Boolean includeInherited,
|
||||
@QueryParam("kind") @Nullable String kind) {
|
||||
boolean inherited = includeInherited != null && includeInherited;
|
||||
return withProject(project, () -> graphRepository.moduleFunctions(project, name, inherited, kind).map(this::ok));
|
||||
return withIngestedModule(project, name, () -> graphRepository.moduleFunctions(project, name, inherited, kind).map(this::ok));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,33 +483,10 @@ public class AnalysisResource {
|
||||
@GET
|
||||
@Path("/modules/{name}/functions/overrides")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = BulkFunctionOverride.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> bulkFunctionOverrides(@PathParam("project") String project, @PathParam("name") String name) {
|
||||
return withProject(project, () -> graphRepository.functionOverrides(project, name).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/functions/{function}/overrides")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = FunctionOverride.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> functionOverrides(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@PathParam("function") String function) {
|
||||
return withProject(project, () -> graphRepository.functionOverrides(project, name, function).map(this::ok));
|
||||
}
|
||||
|
||||
/**
|
||||
* Item 52: the FUNCTION-level callers of a subroutine/method — who {@code PERFORM}s (Natural) or
|
||||
* calls (Java cross-class) {@code function} in module {@code name}. Complements the
|
||||
* module-granularity {@code /callers}.
|
||||
*/
|
||||
@GET
|
||||
@Path("/modules/{name}/functions/{function}/callers")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = CallRefResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> functionCallers(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@PathParam("function") String function) {
|
||||
return withProject(project, () -> graphRepository.functionCallers(project, name, function)
|
||||
.map(resp -> Response.ok(resp).build()));
|
||||
return withIngestedModule(project, name, () -> graphRepository.functionOverrides(project, name).map(this::ok));
|
||||
}
|
||||
|
||||
private static List<NameRef> identifierNames(List<IdentifierMatch> matches) {
|
||||
@@ -558,12 +561,13 @@ public class AnalysisResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/digest")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ModuleDigest.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> moduleDigest(@PathParam("project") String project, @PathParam("name") String name) {
|
||||
return withProject(project, () -> graphRepository.moduleDigest(project, name)
|
||||
.map(digest -> Response.ok(digest).build()));
|
||||
@Path("/modules/{name}/functions/{function}/overrides")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = FunctionOverride.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> functionOverrides(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@PathParam("function") String function) {
|
||||
return withIngestedModule(project, name, () -> graphRepository.functionOverrides(project, name, function).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -578,16 +582,43 @@ public class AnalysisResource {
|
||||
AnalysisResource::stepModules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item 52: the FUNCTION-level callers of a subroutine/method — who {@code PERFORM}s (Natural) or
|
||||
* calls (Java cross-class) {@code function} in module {@code name}. Complements the
|
||||
* module-granularity {@code /callers}.
|
||||
*/
|
||||
@GET
|
||||
@Path("/modules/{name}/functions/{function}/callers")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = CallRefResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested, so it has no FUNCTION nodes.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> functionCallers(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@PathParam("function") String function) {
|
||||
return withIngestedModule(project, name, () -> graphRepository.functionCallers(project, name, function)
|
||||
.map(resp -> Response.ok(resp).build()));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/digest")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ModuleDigest.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested. Its callers are still knowable via /callers.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> moduleDigest(@PathParam("project") String project, @PathParam("name") String name) {
|
||||
return withIngestedModule(project, name, () -> graphRepository.moduleDigest(project, name)
|
||||
.map(digest -> Response.ok(digest).build()));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/callees")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = CallRefResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested, so it has no known callees.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> callees(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("scope") @Nullable String scope,
|
||||
@QueryParam("limit") @Nullable Integer limit, @QueryParam("offset") @Nullable Integer offset,
|
||||
@QueryParam("fields") @Nullable String fields,
|
||||
@QueryParam("resolveInterfaces") @Nullable Boolean resolveInterfaces) {
|
||||
return withProject(project, () -> graphRepository.callees(project, name, scope, effectiveLimit(limit), effectiveOffset(offset),
|
||||
return withIngestedModule(project, name, () -> graphRepository.callees(project, name, scope, effectiveLimit(limit), effectiveOffset(offset),
|
||||
resolveInterfaces != null && resolveInterfaces)
|
||||
.map(resp -> namesOnly(fields)
|
||||
? ok(callRefNames(resp))
|
||||
@@ -597,13 +628,14 @@ public class AnalysisResource {
|
||||
@GET
|
||||
@Path("/modules/{name}/db-accesses")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = DbAccess.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> dbAccesses(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("depth") @Nullable Integer depth,
|
||||
@QueryParam("limit") @Nullable Integer limit, @QueryParam("offset") @Nullable Integer offset) {
|
||||
int effLimit = uncappedLimit(limit);
|
||||
int effOffset = effectiveOffset(offset);
|
||||
return withProject(project, () -> {
|
||||
return withIngestedModule(project, name, () -> {
|
||||
if (depth != null && depth > 0) {
|
||||
return graphRepository.dbAccessesTransitive(project, name, depth, effLimit, effOffset).map(this::ok);
|
||||
}
|
||||
@@ -614,77 +646,32 @@ public class AnalysisResource {
|
||||
@GET
|
||||
@Path("/modules/{name}/workfile-accesses")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = WorkfileAccess.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> workfileAccesses(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("limit") @Nullable Integer limit, @QueryParam("offset") @Nullable Integer offset) {
|
||||
int effLimit = uncappedLimit(limit);
|
||||
int effOffset = effectiveOffset(offset);
|
||||
return withProject(project, () -> graphRepository.workfileAccesses(project, name, effLimit, effOffset).map(this::ok));
|
||||
return withIngestedModule(project, name, () -> graphRepository.workfileAccesses(project, name, effLimit, effOffset).map(this::ok));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/call-tree")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = CallTreeResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@APIResponse(responseCode = "409", description = "Module is an unresolved placeholder — its source is not ingested, so the tree would be empty for want of data, not for want of matches.", content = @Content(schema = @Schema(implementation = DeepIngestRequired.class)))
|
||||
public Uni<Response> callTree(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("depth") @Nullable Integer depth,
|
||||
@QueryParam("fields") @Nullable String fields,
|
||||
@QueryParam("resolveInterfaces") @Nullable Boolean resolveInterfaces,
|
||||
@QueryParam("followWiring") @Nullable Boolean followWiring) {
|
||||
int effectiveDepth = Math.clamp(depth != null ? depth : defaultCallTreeDepth, 1, maxCallTreeDepth);
|
||||
return withFanoutWarm(project,
|
||||
return withIngestedModule(project, name, () -> fanoutWarm(project,
|
||||
() -> graphRepository.callTree(project, name, effectiveDepth,
|
||||
resolveInterfaces != null && resolveInterfaces, followWiring != null && followWiring,
|
||||
callTreeInternalBudget),
|
||||
CallTreeResponse::sourceFiles,
|
||||
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), each <b>rolled up to the calling module</b> — a call made
|
||||
* from inside a subroutine/method is attributed to its owning module (not the calling {@code FUNCTION}
|
||||
* node), and repeated call sites from one caller collapse to a single row whose {@code sites} list
|
||||
* every line (symmetric with {@code callees}); {@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)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> callers(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("scope") @Nullable String scope,
|
||||
@QueryParam("limit") @Nullable Integer limit, @QueryParam("offset") @Nullable Integer offset,
|
||||
@QueryParam("fields") @Nullable String fields) {
|
||||
return withFanoutWarm(project,
|
||||
() -> graphRepository.callers(project, name, scope, effectiveLimit(limit), effectiveOffset(offset)),
|
||||
CallRefResponse::sourceFiles,
|
||||
resp -> namesOnly(fields) ? ok(callRefNames(resp)) : Response.ok(resp).build());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/graph")
|
||||
@Operation(summary = "Ego graph around a module",
|
||||
description = "Item 49: a bounded call-graph neighbourhood (nodes + edges) around one module, "
|
||||
+ "for the web UI's interactive sub-graph. Module-granularity; keyed on name+sourceFile.")
|
||||
@APIResponse(responseCode = "200", description = "The bounded neighbourhood.",
|
||||
content = @Content(schema = @Schema(implementation = EgoGraphResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> egoGraph(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@Parameter(description = "Max call hops from the module (clamped to the call-tree max).")
|
||||
@QueryParam("depth") @Nullable Integer depth,
|
||||
@Parameter(description = "Traversal direction: out (callees), in (callers), or both.")
|
||||
@QueryParam("direction") @Nullable String direction,
|
||||
@Parameter(description = "Cap on the number of nodes returned (BFS order).")
|
||||
@QueryParam("limit") @Nullable Integer limit) {
|
||||
int effectiveDepth = depth != null ? depth : defaultCallTreeDepth;
|
||||
String dir = direction != null ? direction : "out";
|
||||
int effectiveLimit = limit != null ? limit : DEFAULT_PAGE_LIMIT;
|
||||
return withProject(project, () -> graphRepository.egoGraph(project, name, effectiveDepth, dir, effectiveLimit)
|
||||
.map(resp -> Response.ok(resp).build()));
|
||||
resp -> namesOnly(fields) ? ok(callTreeNames(resp)) : Response.ok(resp).build()));
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -798,28 +785,34 @@ public class AnalysisResource {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Module-granularity callers. {@code scope}: {@code external} (default) = modules that call this
|
||||
* module (incoming CALLNAT / inheritance), each <b>rolled up to the calling module</b> — a call made
|
||||
* from inside a subroutine/method is attributed to its owning module (not the calling {@code FUNCTION}
|
||||
* node), and repeated call sites from one caller collapse to a single row whose {@code sites} list
|
||||
* every line (symmetric with {@code callees}); {@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.
|
||||
*
|
||||
* <p>Item 107: this endpoint is guarded by {@link #withModule}, not {@link #withIngestedModule} —
|
||||
* an unresolved <b>placeholder</b> module still answers {@code 200} here, because its callers are
|
||||
* read from the <em>calling</em> modules' source and are genuine. It is the one honest answer
|
||||
* available for a module whose own source was never ingested, so the {@code 409} on the other
|
||||
* endpoints points here.
|
||||
*/
|
||||
@GET
|
||||
@Path("/modules/{name}/source")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = SourceSnippet.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> moduleSource(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("startLine") @Nullable Integer startLine,
|
||||
@QueryParam("endLine") @Nullable Integer endLine) {
|
||||
// M1: omit BOTH bounds to get the whole file (the source-viewer's default). Supplying exactly
|
||||
// one bound is ambiguous and rejected.
|
||||
boolean wholeFile = startLine == null && endLine == null;
|
||||
if (!wholeFile && (startLine == null || endLine == null)) {
|
||||
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "MISSING_LINE_RANGE",
|
||||
"Supply both 'startLine' and 'endLine', or neither (whole file)"));
|
||||
}
|
||||
int sl = startLine != null ? startLine : 1;
|
||||
int el = endLine != null ? endLine : Integer.MAX_VALUE;
|
||||
return withProject(project, () -> graphRepository.moduleSourceFile(project, name).flatMap(sourceFile -> sourceFile == null
|
||||
? Uni.createFrom().item(ProjectResource.error(Response.Status.NOT_FOUND, "MODULE_NOT_FOUND",
|
||||
"No module '" + name + "' in project '" + project + "'"))
|
||||
: wholeFile
|
||||
? sourceSnippetWhole(project, sourceFile)
|
||||
: sourceSnippet(project, sourceFile, sl, el)));
|
||||
@Path("/modules/{name}/callers")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = CallRefResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> callers(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("scope") @Nullable String scope,
|
||||
@QueryParam("limit") @Nullable Integer limit, @QueryParam("offset") @Nullable Integer offset,
|
||||
@QueryParam("fields") @Nullable String fields) {
|
||||
return withModule(project, name, () -> fanoutWarm(project,
|
||||
() -> graphRepository.callers(project, name, scope, effectiveLimit(limit), effectiveOffset(offset)),
|
||||
CallRefResponse::sourceFiles,
|
||||
resp -> namesOnly(fields) ? ok(callRefNames(resp)) : Response.ok(resp).build()));
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -860,6 +853,104 @@ public class AnalysisResource {
|
||||
"Project '" + project + "' does not exist")));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/graph")
|
||||
@Operation(summary = "Ego graph around a module",
|
||||
description = "Item 49: a bounded call-graph neighbourhood (nodes + edges) around one module, "
|
||||
+ "for the web UI's interactive sub-graph. Module-granularity; keyed on name+sourceFile.")
|
||||
@APIResponse(responseCode = "200", description = "The bounded neighbourhood.",
|
||||
content = @Content(schema = @Schema(implementation = EgoGraphResponse.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project or module not found.",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> egoGraph(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@Parameter(description = "Max call hops from the module (clamped to the call-tree max).")
|
||||
@QueryParam("depth") @Nullable Integer depth,
|
||||
@Parameter(description = "Traversal direction: out (callees), in (callers), or both.")
|
||||
@QueryParam("direction") @Nullable String direction,
|
||||
@Parameter(description = "Cap on the number of nodes returned (BFS order).")
|
||||
@QueryParam("limit") @Nullable Integer limit) {
|
||||
int effectiveDepth = depth != null ? depth : defaultCallTreeDepth;
|
||||
String dir = direction != null ? direction : "out";
|
||||
int effectiveLimit = limit != null ? limit : DEFAULT_PAGE_LIMIT;
|
||||
// Item 107: withModule, not withIngestedModule — a placeholder legitimately has an incoming
|
||||
// neighbourhood (that is why the node exists at all), and the UI needs to render it.
|
||||
return withModule(project, name, () -> graphRepository.egoGraph(project, name, effectiveDepth, dir, effectiveLimit)
|
||||
.map(resp -> Response.ok(resp).build()));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/modules/{name}/source")
|
||||
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = SourceSnippet.class)))
|
||||
@APIResponse(responseCode = "404", description = "Project not found.", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public Uni<Response> moduleSource(@PathParam("project") String project, @PathParam("name") String name,
|
||||
@QueryParam("startLine") @Nullable Integer startLine,
|
||||
@QueryParam("endLine") @Nullable Integer endLine) {
|
||||
// M1: omit BOTH bounds to get the whole file (the source-viewer's default). Supplying exactly
|
||||
// one bound is ambiguous and rejected.
|
||||
boolean wholeFile = startLine == null && endLine == null;
|
||||
if (!wholeFile && (startLine == null || endLine == null)) {
|
||||
return Uni.createFrom().item(ProjectResource.error(Response.Status.BAD_REQUEST, "MISSING_LINE_RANGE",
|
||||
"Supply both 'startLine' and 'endLine', or neither (whole file)"));
|
||||
}
|
||||
int sl = startLine != null ? startLine : 1;
|
||||
int el = endLine != null ? endLine : Integer.MAX_VALUE;
|
||||
// Item 107: already module-aware — moduleSourceFile filters `sourceFile <> ""`, so both an
|
||||
// absent module and an unresolved placeholder land on MODULE_NOT_FOUND. Kept as-is (there is
|
||||
// no source to serve either way); only the error construction is shared with the new guards.
|
||||
return withProject(project, () -> graphRepository.moduleSourceFile(project, name).flatMap(sourceFile -> sourceFile == null
|
||||
? Uni.createFrom().item(moduleNotFound(project, name))
|
||||
: wholeFile
|
||||
? sourceSnippetWhole(project, sourceFile)
|
||||
: sourceSnippet(project, sourceFile, sl, el)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Item 107: runs {@code action} only if the project exists <em>and</em> the graph knows a
|
||||
* {@code MODULE} node for {@code name} — real or placeholder — otherwise
|
||||
* {@code 404 MODULE_NOT_FOUND}.
|
||||
*
|
||||
* <p>Before this guard every {@code /modules/{name}/…} endpoint answered {@code 200} with an
|
||||
* all-zeros shell for a name typed at random, byte-identical to a real module's answer. That is
|
||||
* not merely an ugly response: an agent asking "does this module reach the commission
|
||||
* calculation?" read the empty {@code 200} as <em>"analysed, nothing found"</em> when the truth
|
||||
* was <em>"not analysable"</em>.
|
||||
*
|
||||
* <p>Use this for endpoints whose answer is derived from the <em>calling</em> modules' source and
|
||||
* is therefore genuine even for a placeholder ({@code /callers}, {@code /graph}). Endpoints that
|
||||
* read the module's <b>own</b> source must use {@link #withIngestedModule} instead, so a
|
||||
* placeholder does not silently degrade to an empty-but-successful answer.
|
||||
*/
|
||||
private Uni<Response> withModule(String project, String name, Supplier<Uni<Response>> action) {
|
||||
return withProject(project, () -> graphRepository.moduleIngestState(project, name)
|
||||
.flatMap(state -> state.present()
|
||||
? action.get()
|
||||
: Uni.createFrom().item(moduleNotFound(project, name))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Item 107: like {@link #withModule}, but additionally rejects an unresolved <b>placeholder</b>
|
||||
* module (a node that exists only because something calls it — its source was never parsed) with
|
||||
* {@code 409 NOT_INGESTED} and an actionable {@code nextAction}.
|
||||
*
|
||||
* <p>For such a module everything derived from its own source — functions, DB accesses, data
|
||||
* structures, dispatch table, callees, call tree — is structurally empty, so a {@code 200} would
|
||||
* be exactly the "looks complete but isn't" answer this item exists to remove. Nothing is lost by
|
||||
* refusing: the deep-ingest warm on {@code call-tree}/{@code callers} is seeded from the source
|
||||
* files a result surfaced, and a placeholder surfaces none. Callers wanting the part that <em>is</em>
|
||||
* knowable should use {@code /callers}, which stays {@code 200}.
|
||||
*/
|
||||
private Uni<Response> withIngestedModule(String project, String name, Supplier<Uni<Response>> action) {
|
||||
return withProject(project, () -> graphRepository.moduleIngestState(project, name).flatMap(state -> {
|
||||
if (!state.present()) {
|
||||
return Uni.createFrom().item(moduleNotFound(project, name));
|
||||
}
|
||||
if (state.placeholder()) {
|
||||
return Uni.createFrom().item(notIngested(project, name));
|
||||
}
|
||||
return action.get();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up {@code sourceFile}'s stored content hash (item 41), then reads its {@code [startLine,
|
||||
* endLine]} slice with a stale check (see {@link #withSnippet}).
|
||||
@@ -944,9 +1035,21 @@ public class AnalysisResource {
|
||||
private <T> Uni<Response> withFanoutWarm(String project, Supplier<Uni<T>> query,
|
||||
Function<T, Collection<String>> surfaced,
|
||||
Function<T, Response> render) {
|
||||
return withProject(project, () -> query.get().flatMap(first ->
|
||||
return withProject(project, () -> fanoutWarm(project, query, surfaced, render));
|
||||
}
|
||||
|
||||
/**
|
||||
* The guard-free body of {@link #withFanoutWarm}. Split out for item 107 so a module-scoped
|
||||
* endpoint can wrap the same warm-and-rerun logic in {@link #withModule} /
|
||||
* {@link #withIngestedModule} instead of the project-only guard — and so the module check runs
|
||||
* <b>once, before</b> the first query rather than again on the warm-triggered re-run.
|
||||
*/
|
||||
private <T> Uni<Response> fanoutWarm(String project, Supplier<Uni<T>> query,
|
||||
Function<T, Collection<String>> surfaced,
|
||||
Function<T, Response> render) {
|
||||
return query.get().flatMap(first ->
|
||||
deepIngestCoordinator.ensureDeepMany(project, surfaced.apply(first)).flatMap(changed ->
|
||||
changed ? query.get().map(render) : Uni.createFrom().item(render.apply(first)))));
|
||||
changed ? query.get().map(render) : Uni.createFrom().item(render.apply(first))));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -375,7 +375,7 @@ public class DeepIngestCoordinator {
|
||||
if (state.isFull()) {
|
||||
return;
|
||||
}
|
||||
if (!state.exists()) {
|
||||
if (!state.ingested()) {
|
||||
// No coarse node to carry a durable status; the monitor still prevents an intra-JVM
|
||||
// duplicate. Ingest best-effort and stop.
|
||||
ingestClaimed(project, module);
|
||||
|
||||
@@ -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=151
|
||||
agenticcode.version=156
|
||||
# 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
|
||||
|
||||
@@ -71,7 +71,7 @@ class DeepIngestStatusIT {
|
||||
void durableIngestStatusLifecycle() {
|
||||
// After a call-graph ingest the module exists but is not deeply ingested.
|
||||
ModuleIngestState initial = state();
|
||||
assertTrue(initial.exists(), "call-graph ingest should create a real module node");
|
||||
assertTrue(initial.ingested(), "call-graph ingest should create a real module node");
|
||||
assertFalse(initial.isFull(), "call-graph ingest is not a deep (FULL) ingest");
|
||||
assertEquals(IngestStatus.NOT_INGESTED.name(), initial.status());
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.agenticcode.codeserver.api;
|
||||
|
||||
import com.agenticcode.neo4jstore.graph.GraphRepository;
|
||||
import com.agenticcode.neo4jstore.graph.ModuleIngestState;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.RestAssured;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Item 107: a {@code /modules/{name}/…} endpoint must never invent an empty module.
|
||||
*
|
||||
* <p>Before this guard, a name typed at random and a real-but-empty module produced byte-identical
|
||||
* {@code 200}s, so an agent read <em>"not analysable"</em> as <em>"analysed, nothing found"</em>.
|
||||
* The graph knows three states and each now gets its own answer:
|
||||
* <ul>
|
||||
* <li>absent → {@code 404 MODULE_NOT_FOUND}</li>
|
||||
* <li>unresolved placeholder (referenced by a caller, source never parsed) →
|
||||
* {@code 409 NOT_INGESTED}, except on {@code /callers} and {@code /graph}, whose data comes
|
||||
* from the <em>calling</em> modules and is genuine</li>
|
||||
* <li>real, ingested module → {@code 200} (regression guard: the guards must not break the
|
||||
* happy path)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@QuarkusTest
|
||||
class ModuleNotFoundIT {
|
||||
|
||||
private static final String PROJECT = "item107-module-not-found";
|
||||
/**
|
||||
* A real, parsed module.
|
||||
*/
|
||||
private static final String REAL = "MNF_REAL";
|
||||
/**
|
||||
* Referenced by {@link #REAL} but never present on disk — the graph holds only a placeholder.
|
||||
*/
|
||||
private static final String GHOST = "MNF_GHOST";
|
||||
/**
|
||||
* Not in the graph at all.
|
||||
*/
|
||||
private static final String ABSENT = "NOSUCHMOD123";
|
||||
|
||||
@TempDir
|
||||
static Path root;
|
||||
|
||||
@Inject
|
||||
GraphRepository graphRepository;
|
||||
|
||||
/**
|
||||
* Endpoints whose answer is derived from the module's <b>own</b> source: {@code 404} when the
|
||||
* module is absent, {@code 409} when it is a placeholder.
|
||||
*/
|
||||
static List<String> sourceDependentEndpoints() {
|
||||
return List.of("digest", "context", "call-tree", "callees", "db-accesses", "workfile-accesses",
|
||||
"sql-statements", "functions", "functions/overrides", "functions/SOME-FN/overrides",
|
||||
"functions/SOME-FN/callers", "data-structures", "dispatch-table", "payload", "columns");
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoints answerable from the <b>calling</b> modules' source: {@code 404} when the module is
|
||||
* absent, but {@code 200} for a placeholder — the data is real.
|
||||
*/
|
||||
static List<String> callerDerivedEndpoints() {
|
||||
return List.of("callers", "graph");
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void createProject() {
|
||||
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
|
||||
// MNF_REAL calls MNF_GHOST, whose source is deliberately absent from the root: the call-graph
|
||||
// enrichment resolves the CALLNAT onto a placeholder MODULE node (sourceFile = "").
|
||||
write(REAL + ".nat", """
|
||||
DEFINE DATA
|
||||
LOCAL
|
||||
1 #CLIENT (A8)
|
||||
END-DEFINE
|
||||
*
|
||||
CALLNAT '""" + GHOST + """
|
||||
' #CLIENT
|
||||
*
|
||||
END
|
||||
""");
|
||||
given().contentType("application/json")
|
||||
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natural", null, null))
|
||||
.when().post("/api/projects/" + PROJECT)
|
||||
.then().statusCode(201);
|
||||
}
|
||||
|
||||
private static void write(String fileName, String content) {
|
||||
try {
|
||||
Files.write(root.resolve(fileName), content.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String path(String module, String endpoint) {
|
||||
return "/api/projects/" + PROJECT + "/modules/" + module + "/" + endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* The premise the placeholder cases rest on. Asserted separately so a change in how unresolved
|
||||
* {@code CALLNAT} targets are stored fails here with a clear message, rather than silently
|
||||
* turning every {@code 409} assertion below into a vacuous {@code 404}.
|
||||
*/
|
||||
@Test
|
||||
void fixtureProducesTheThreeStates() {
|
||||
ModuleIngestState real = state(REAL);
|
||||
assertTrue(real.present(), REAL + " must be in the graph");
|
||||
assertTrue(real.ingested(), REAL + " must be a real, parsed node");
|
||||
assertFalse(real.placeholder(), REAL + " must not be a placeholder");
|
||||
|
||||
ModuleIngestState ghost = state(GHOST);
|
||||
assertTrue(ghost.present(), GHOST + " must exist as a node — something calls it");
|
||||
assertTrue(ghost.placeholder(), GHOST + " must be an unresolved placeholder (sourceFile = \"\")");
|
||||
assertFalse(ghost.ingested(), GHOST + " source is absent, so it is not ingested");
|
||||
|
||||
assertFalse(state(ABSENT).present(), ABSENT + " must not be in the graph at all");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource({"sourceDependentEndpoints", "callerDerivedEndpoints"})
|
||||
void absentModuleIs404(String endpoint) {
|
||||
given().when().get(path(ABSENT, endpoint))
|
||||
.then().statusCode(404)
|
||||
.body("code", equalTo("MODULE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("sourceDependentEndpoints")
|
||||
void placeholderModuleIs409(String endpoint) {
|
||||
given().when().get(path(GHOST, endpoint))
|
||||
.then().statusCode(409)
|
||||
.body("status", equalTo("NOT_INGESTED"))
|
||||
.body("module", equalTo(GHOST))
|
||||
.body("nextAction.method", equalTo("POST"))
|
||||
.body("nextAction.path", equalTo("/api/projects/" + PROJECT + "/ingest/" + GHOST));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("callerDerivedEndpoints")
|
||||
void placeholderModuleStays200WhereTheDataIsReal(String endpoint) {
|
||||
given().when().get(path(GHOST, endpoint)).then().statusCode(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* The placeholder's callers are not merely {@code 200} — they carry the real answer, which is why
|
||||
* this endpoint is exempt from the {@code 409} and why that response points callers here.
|
||||
*/
|
||||
@Test
|
||||
void placeholderCallersCarryRealData() {
|
||||
given().when().get(path(GHOST, "callers"))
|
||||
.then().statusCode(200)
|
||||
.body("items.name", hasItem(REAL));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource({"sourceDependentEndpoints", "callerDerivedEndpoints"})
|
||||
void realModuleStillAnswers200(String endpoint) {
|
||||
given().when().get(path(REAL, endpoint)).then().statusCode(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* An unknown project still wins over the module check — the outer guard runs first.
|
||||
*/
|
||||
@Test
|
||||
void unknownProjectStillReportsProjectNotFound() {
|
||||
given().when().get("/api/projects/no-such-project-107/modules/" + REAL + "/digest")
|
||||
.then().statusCode(404)
|
||||
.body("code", equalTo("PROJECT_NOT_FOUND"));
|
||||
}
|
||||
|
||||
private ModuleIngestState state(String module) {
|
||||
return graphRepository.moduleIngestState(PROJECT, module).await().indefinitely();
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class Tier1IndexIT {
|
||||
void tier1ScanIndexesModulesReferencesAndIdentifiers() {
|
||||
// Modules exist from the coarse scan alone, at the shallow (call-graph / not-ingested) tier.
|
||||
ModuleIngestState caller = state("DF_CALLER");
|
||||
assertTrue(caller.exists(), "Tier-1 scan creates a real module node on create");
|
||||
assertTrue(caller.ingested(), "Tier-1 scan creates a real module node on create");
|
||||
assertFalse(caller.isFull(), "Tier-1 is a coarse scan, not a deep (FULL) ingest");
|
||||
assertEquals(IngestStatus.NOT_INGESTED.name(), caller.status(), "coarse-scanned module is NOT_INGESTED");
|
||||
|
||||
|
||||
@@ -1423,7 +1423,10 @@ public class GraphRepository {
|
||||
/**
|
||||
* @return the ingest state of module {@code name}: whether a real (non-placeholder) node exists
|
||||
* and its {@link IngestDepth}. Used by query endpoints to tell an agent whether a deep ingest is
|
||||
* still required before deep (field/DB/dataflow) data is available.
|
||||
* still required before deep (field/DB/dataflow) data is available, and (item 107) to tell an
|
||||
* absent module apart from an unresolved placeholder — see {@link ModuleIngestState}. A missing
|
||||
* {@code sourceFile} column is normalised to {@code ""}, i.e. treated as a placeholder: the node
|
||||
* exists, it just cannot be analysed.
|
||||
*/
|
||||
public Uni<ModuleIngestState> moduleIngestState(String project, String name) {
|
||||
return Uni.createFrom().item(() -> {
|
||||
@@ -1431,14 +1434,14 @@ public class GraphRepository {
|
||||
return session.executeRead(tx -> {
|
||||
var result = tx.run(CypherQueries.MODULE_INGEST_STATE, Map.of("project", project, "name", name));
|
||||
if (!result.hasNext()) {
|
||||
return new ModuleIngestState(false, null, null, null);
|
||||
return new ModuleIngestState(false, null, null, null, null);
|
||||
}
|
||||
Record record = result.next();
|
||||
boolean real = !record.get("sourceFile").isNull() && !record.get("sourceFile").asString().isEmpty();
|
||||
String sourceFile = record.get("sourceFile").isNull() ? "" : record.get("sourceFile").asString();
|
||||
@Nullable String depth = record.get("depth").isNull() ? null : record.get("depth").asString();
|
||||
@Nullable String status = record.get("status").isNull() ? null : record.get("status").asString();
|
||||
@Nullable Long statusAt = record.get("statusAt").isNull() ? null : record.get("statusAt").asLong();
|
||||
return new ModuleIngestState(real, depth, status, statusAt);
|
||||
return new ModuleIngestState(!sourceFile.isEmpty(), depth, status, statusAt, sourceFile);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,23 +6,58 @@ import org.jspecify.annotations.Nullable;
|
||||
* The ingest state of a module, used by query endpoints to guide an agent on whether a deep ingest
|
||||
* is still required, and by {@code DeepIngestCoordinator} to coalesce concurrent deep ingests.
|
||||
*
|
||||
* @param exists whether a real (non-placeholder, {@code sourceFile != ""}) {@code MODULE} node
|
||||
* exists for the name
|
||||
* @param depth the stored {@code ingestDepth} ({@code "CALL_GRAPH"}/{@code "FULL"}), or {@code null}
|
||||
* if unset (treated as not deeply ingested)
|
||||
* @param status the stored {@code ingestStatus} ({@code "NOT_INGESTED"}/{@code "INGESTING"}/
|
||||
* {@code "INGESTED"}, see {@link IngestStatus}), or {@code null} if unset
|
||||
* @param statusAt the epoch-millis stamp {@code ingestStatusAt} of the last status transition, or
|
||||
* {@code null} if unset — used to reclaim a stale {@code INGESTING} left by a crash
|
||||
* <p>Item 107: the graph knows <b>three</b> states for a module name, and query endpoints must tell
|
||||
* them apart — conflating them is what let every {@code /modules/{name}/…} endpoint answer {@code 200}
|
||||
* with an all-zeros shell for a name that exists nowhere:
|
||||
* <ul>
|
||||
* <li><b>absent</b> — no {@code MODULE} node at all ({@link #present()} is {@code false}); the name
|
||||
* is a typo or simply unknown. Endpoints answer {@code 404 MODULE_NOT_FOUND}.</li>
|
||||
* <li><b>placeholder</b> — a node exists because something calls it, but its source was never
|
||||
* parsed ({@link #placeholder()}). Anything derived from the module's <em>own</em> source is
|
||||
* structurally empty, so endpoints answer {@code 409 NOT_INGESTED} rather than "analysed,
|
||||
* nothing found". Data derived from the <em>calling</em> modules (notably {@code /callers})
|
||||
* is genuine and still served.</li>
|
||||
* <li><b>ingested</b> — a real, parsed node ({@link #ingested()}), possibly only to
|
||||
* {@code CALL_GRAPH} depth (see {@link #isFull()}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param ingested whether a real (non-placeholder, {@code sourceFile != ""}) {@code MODULE} node
|
||||
* exists for the name — i.e. its source was actually parsed
|
||||
* @param depth the stored {@code ingestDepth} ({@code "CALL_GRAPH"}/{@code "FULL"}), or {@code null}
|
||||
* if unset (treated as not deeply ingested)
|
||||
* @param status the stored {@code ingestStatus} ({@code "NOT_INGESTED"}/{@code "INGESTING"}/
|
||||
* {@code "INGESTED"}, see {@link IngestStatus}), or {@code null} if unset
|
||||
* @param statusAt the epoch-millis stamp {@code ingestStatusAt} of the last status transition, or
|
||||
* {@code null} if unset — used to reclaim a stale {@code INGESTING} left by a crash
|
||||
* @param sourceFile the node's {@code sourceFile}: {@code null} when no node exists at all,
|
||||
* {@code ""} for a placeholder, the relative path for a real node
|
||||
*/
|
||||
public record ModuleIngestState(boolean exists, @Nullable String depth,
|
||||
@Nullable String status, @Nullable Long statusAt) {
|
||||
public record ModuleIngestState(boolean ingested, @Nullable String depth,
|
||||
@Nullable String status, @Nullable Long statusAt,
|
||||
@Nullable String sourceFile) {
|
||||
|
||||
/**
|
||||
* @return whether <em>any</em> {@code MODULE} node exists for the name — real or placeholder.
|
||||
* The negation is the {@code 404 MODULE_NOT_FOUND} case: the name is unknown to the graph.
|
||||
*/
|
||||
public boolean present() {
|
||||
return sourceFile != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the only node for this name is an unresolved placeholder: referenced by a
|
||||
* caller, but its source never parsed. Distinct from {@link #present()} being {@code false} —
|
||||
* the module is known to exist, it just cannot be analysed.
|
||||
*/
|
||||
public boolean placeholder() {
|
||||
return sourceFile != null && sourceFile.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the module has been deeply (fully) ingested.
|
||||
*/
|
||||
public boolean isFull() {
|
||||
return exists && IngestDepth.FULL.name().equals(depth);
|
||||
return ingested && IngestDepth.FULL.name().equals(depth);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +66,7 @@ public record ModuleIngestState(boolean exists, @Nullable String depth,
|
||||
* missing stamp is treated as stale (reclaimable), never as freshly ingesting.
|
||||
*/
|
||||
public boolean isIngesting(long staleBeforeMillis) {
|
||||
return exists && IngestStatus.INGESTING.name().equals(status)
|
||||
return ingested && IngestStatus.INGESTING.name().equals(status)
|
||||
&& statusAt != null && statusAt >= staleBeforeMillis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,37 @@ import {api} from "./client";
|
||||
* (name, sourceFile) stable identity — node ids are regenerated on re-ingest, so we never key on them.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Item 107: the server no longer answers `200` with an empty shell for a module it cannot analyse.
|
||||
* A module endpoint now returns `404 MODULE_NOT_FOUND` (no such module) or `409 NOT_INGESTED` (an
|
||||
* unresolved placeholder — referenced by a caller, but its own source was never parsed).
|
||||
*
|
||||
* These are the message strings the panels test for. Kept as sentinels rather than prose so a panel
|
||||
* can tell "this module cannot be analysed" apart from "the request failed", and render one banner
|
||||
* instead of a wall of red — see {@link moduleError}.
|
||||
*/
|
||||
export const MODULE_NOT_FOUND = "MODULE_NOT_FOUND";
|
||||
export const MODULE_NOT_INGESTED = "MODULE_NOT_INGESTED";
|
||||
|
||||
/** True when the error is the server saying this module cannot be analysed, not that a call failed. */
|
||||
export function isModuleUnavailable(error: unknown): boolean {
|
||||
return error instanceof Error
|
||||
&& (error.message === MODULE_NOT_FOUND || error.message === MODULE_NOT_INGESTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a failed module-endpoint response to an Error, promoting the two item-107 statuses to the
|
||||
* sentinels above. Every module hook throws through here so the distinction survives to the UI;
|
||||
* `fallback` covers everything else (network, 5xx) and keeps the old per-panel message.
|
||||
*
|
||||
* Mirrors the `STALE_SOURCE` handling in {@link useModuleSource}, which predates this.
|
||||
*/
|
||||
function moduleError(status: number, fallback: string): Error {
|
||||
if (status === 404) return new Error(MODULE_NOT_FOUND);
|
||||
if (status === 409) return new Error(MODULE_NOT_INGESTED);
|
||||
return new Error(fallback);
|
||||
}
|
||||
|
||||
export function useProjects() {
|
||||
return useQuery({
|
||||
queryKey: ["projects"],
|
||||
@@ -47,10 +78,10 @@ export function useModuleContext(project: string | undefined, name: string | und
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["context", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/context", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/context", {
|
||||
params: {path: {project: project!, name: name!}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load module context");
|
||||
if (error) throw moduleError(response.status, "Failed to load module context");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -62,10 +93,10 @@ export function useModuleFunctions(project: string | undefined, name: string | u
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["functions", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/functions", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/functions", {
|
||||
params: {path: {project: project!, name: name!}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load functions");
|
||||
if (error) throw moduleError(response.status, "Failed to load functions");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -118,11 +149,11 @@ export function useCalls(project: string | undefined, name: string | undefined,
|
||||
queryKey: [dir, project, name],
|
||||
queryFn: async () => {
|
||||
const params = {path: {project: project!, name: name!}, query: {scope: "external"}};
|
||||
const {data, error} =
|
||||
const {data, error, response} =
|
||||
dir === "callers"
|
||||
? await api.GET("/api/projects/{project}/modules/{name}/callers", {params})
|
||||
: await api.GET("/api/projects/{project}/modules/{name}/callees", {params});
|
||||
if (error) throw new Error(`Failed to load ${dir}`);
|
||||
if (error) throw moduleError(response.status, `Failed to load ${dir}`);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -138,10 +169,10 @@ export function useInternalCallees(project: string | undefined, module: string |
|
||||
enabled: enabled && !!project && !!module,
|
||||
queryKey: ["callees-internal", project, module],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/callees", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/callees", {
|
||||
params: {path: {project: project!, name: module!}, query: {scope: "internal"}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load internal callees");
|
||||
if (error) throw moduleError(response.status, "Failed to load internal callees");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -162,11 +193,11 @@ export function useFunctionCallers(
|
||||
enabled: enabled && !!project && !!module && !!fn,
|
||||
queryKey: ["function-callers", project, module, fn],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET(
|
||||
const {data, error, response} = await api.GET(
|
||||
"/api/projects/{project}/modules/{name}/functions/{function}/callers",
|
||||
{params: {path: {project: project!, name: module!, function: fn!}}},
|
||||
);
|
||||
if (error) throw new Error("Failed to load function callers");
|
||||
if (error) throw moduleError(response.status, "Failed to load function callers");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -219,10 +250,10 @@ export function useModulePayload(project: string | undefined, name: string | und
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["payload", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/payload", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/payload", {
|
||||
params: {path: {project: project!, name: name!}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load payload");
|
||||
if (error) throw moduleError(response.status, "Failed to load payload");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -233,10 +264,10 @@ export function useModuleDataStructures(project: string | undefined, name: strin
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["data-structures", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/data-structures", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/data-structures", {
|
||||
params: {path: {project: project!, name: name!}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load data structures");
|
||||
if (error) throw moduleError(response.status, "Failed to load data structures");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -262,10 +293,10 @@ export function useDbAccesses(project: string | undefined, name: string | undefi
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["db-accesses", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/db-accesses", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/db-accesses", {
|
||||
params: {path: {project: project!, name: name!}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load DB accesses");
|
||||
if (error) throw moduleError(response.status, "Failed to load DB accesses");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -276,10 +307,10 @@ export function useSqlStatements(project: string | undefined, name: string | und
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["sql-statements", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/sql-statements", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/sql-statements", {
|
||||
params: {path: {project: project!, name: name!}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load SQL statements");
|
||||
if (error) throw moduleError(response.status, "Failed to load SQL statements");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -290,10 +321,10 @@ export function useDispatchTable(project: string | undefined, name: string | und
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["dispatch-table", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/dispatch-table", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/dispatch-table", {
|
||||
params: {path: {project: project!, name: name!}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load dispatch table");
|
||||
if (error) throw moduleError(response.status, "Failed to load dispatch table");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -437,10 +468,10 @@ export function useImpactCallers(project: string | undefined, name: string | und
|
||||
enabled: !!project && !!name,
|
||||
queryKey: ["impact-callers", project, name],
|
||||
queryFn: async () => {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/graph", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/graph", {
|
||||
params: {path: {project: project!, name: name!}, query: {direction: "in", depth: 10, limit: 500}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load impact");
|
||||
if (error) throw moduleError(response.status, "Failed to load impact");
|
||||
return data;
|
||||
},
|
||||
});
|
||||
@@ -457,9 +488,9 @@ export async function fetchEgoGraph(
|
||||
depth: number,
|
||||
limit: number,
|
||||
) {
|
||||
const {data, error} = await api.GET("/api/projects/{project}/modules/{name}/graph", {
|
||||
const {data, error, response} = await api.GET("/api/projects/{project}/modules/{name}/graph", {
|
||||
params: {path: {project, name}, query: {direction, depth, limit}},
|
||||
});
|
||||
if (error) throw new Error("Failed to load graph");
|
||||
if (error) throw moduleError(response.status, "Failed to load graph");
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,9 @@ export interface paths {
|
||||
/** Data Structure Fields */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
query?: {
|
||||
sourceFile?: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
name: string;
|
||||
@@ -289,6 +291,178 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project}/dynamic-calls/overrides": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List manual dynamic-CALLNAT overrides (item 82), each flagged obsolete if auto-resolution has since caught up. */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
project: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DynamicCallOverride"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
/** Pin a dynamic CALLNAT call site (originFile+lineNo) to one or more target modules (item 82); applied immediately. */
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
project: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DynamicCallOverrideRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DynamicCallOverride"];
|
||||
};
|
||||
};
|
||||
/** @description Missing field or unknown target module. */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Reset dynamic-CALLNAT overrides (item 82): one call site via originFile+lineNo, or all when both are omitted; restores the unresolved placeholder inline. */
|
||||
delete: {
|
||||
parameters: {
|
||||
query?: {
|
||||
lineNo?: number;
|
||||
originFile?: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
project: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ResetResult"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project}/dynamic-calls/unresolved": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List unresolved dynamic CALLNAT call sites (item 82) a human/agent can pin a target to. */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
project: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["UnresolvedDynamicCall"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project}/loc": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -424,7 +598,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["CallTreeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -433,6 +607,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested, so the tree would be empty for want of data, not for want of matches. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -478,7 +661,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["CallRefResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -487,6 +670,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested, so it has no known callees. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -531,7 +723,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["CallRefResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -579,7 +771,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["EntityColumn"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -588,6 +780,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -631,7 +832,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ModuleContext"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -640,6 +841,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -679,7 +889,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ModuleDataStructure"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -688,6 +898,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -731,7 +950,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["DbAccess"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -740,6 +959,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -779,7 +1007,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ModuleDigest"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -788,6 +1016,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. Its callers are still knowable via /callers. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -827,7 +1064,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["DispatchEntry"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -836,6 +1073,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -878,7 +1124,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["InheritedFunction"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -887,6 +1133,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -926,7 +1181,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["BulkFunctionOverride"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -935,6 +1190,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -975,7 +1239,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["CallRefResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -984,6 +1248,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested, so it has no FUNCTION nodes. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -1024,7 +1297,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["FunctionOverride"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -1033,6 +1306,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -1082,7 +1364,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["EgoGraphResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -1130,7 +1412,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["PayloadField"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -1139,6 +1421,15 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -1231,7 +1522,7 @@ export interface paths {
|
||||
"application/json": components["schemas"]["SqlStatement"][];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -1240,6 +1531,75 @@ export interface paths {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project}/modules/{name}/workfile-accesses": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Workfile Accesses */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
name: string;
|
||||
project: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["WorkfileAccess"][];
|
||||
};
|
||||
};
|
||||
/** @description Project or module not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Module is an unresolved placeholder — its source is not ingested. */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeepIngestRequired"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
@@ -1346,6 +1706,64 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project}/recreate": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Recreate */
|
||||
post: {
|
||||
parameters: {
|
||||
query?: {
|
||||
deep?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
project: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["IngestSummary"];
|
||||
};
|
||||
};
|
||||
/** @description The project's stored root no longer resolves; nothing was deleted. */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Project not found. */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project}/refresh": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2017,6 +2435,15 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
AccessSite: {
|
||||
/** Format: int32 */
|
||||
lineNo?: number;
|
||||
sourceFile?: string;
|
||||
viaCopycode?: boolean;
|
||||
/** Format: int32 */
|
||||
includedAt?: number;
|
||||
includePath?: components["schemas"]["IncludeStep"][];
|
||||
};
|
||||
AggregatedCallRef: {
|
||||
name?: string;
|
||||
type?: components["schemas"]["NodeType"];
|
||||
@@ -2024,6 +2451,7 @@ export interface components {
|
||||
sourceFileIndex?: number;
|
||||
edgeKind?: string;
|
||||
sites?: components["schemas"]["CallSite"][];
|
||||
unresolved?: boolean;
|
||||
};
|
||||
AnnotationMatch: {
|
||||
id?: string;
|
||||
@@ -2058,6 +2486,7 @@ export interface components {
|
||||
viaCopycode?: string;
|
||||
/** Format: int32 */
|
||||
includedAt?: number;
|
||||
includePath?: components["schemas"]["IncludeStep"][];
|
||||
};
|
||||
CallTreeItem: {
|
||||
name?: string;
|
||||
@@ -2083,6 +2512,7 @@ export interface components {
|
||||
/** Format: int32 */
|
||||
endLine?: number;
|
||||
scope?: string;
|
||||
sourceFile?: string;
|
||||
};
|
||||
DataStructureRef: {
|
||||
name?: string;
|
||||
@@ -2101,6 +2531,13 @@ export interface components {
|
||||
mode?: string;
|
||||
lineNos?: number[];
|
||||
via?: string;
|
||||
sites?: components["schemas"]["AccessSite"][];
|
||||
};
|
||||
DeepIngestRequired: {
|
||||
status?: string;
|
||||
module?: string;
|
||||
detail?: string;
|
||||
nextAction?: components["schemas"]["NextAction"];
|
||||
};
|
||||
DispatchEntry: {
|
||||
guards?: components["schemas"]["DispatchGuard"][];
|
||||
@@ -2121,6 +2558,25 @@ export interface components {
|
||||
kind?: components["schemas"]["NodeType"];
|
||||
paths?: string[];
|
||||
};
|
||||
DynamicCallOverride: {
|
||||
originFile?: string;
|
||||
/** Format: int32 */
|
||||
lineNo?: number;
|
||||
variable?: string;
|
||||
targets?: string[];
|
||||
note?: string;
|
||||
createdBy?: string;
|
||||
createdAt?: string;
|
||||
obsolete?: boolean;
|
||||
};
|
||||
DynamicCallOverrideRequest: {
|
||||
originFile?: string;
|
||||
/** Format: int32 */
|
||||
lineNo?: number;
|
||||
targets?: string[];
|
||||
variable?: string;
|
||||
note?: string;
|
||||
};
|
||||
/** @description A bounded call-graph neighbourhood (nodes + edges) around one module. */
|
||||
EgoGraphResponse: {
|
||||
root?: string;
|
||||
@@ -2168,6 +2624,7 @@ export interface components {
|
||||
};
|
||||
FunctionInfo: {
|
||||
name?: string;
|
||||
sourceFile?: string;
|
||||
/** Format: int32 */
|
||||
startLine?: number;
|
||||
/** Format: int32 */
|
||||
@@ -2211,6 +2668,11 @@ export interface components {
|
||||
scope?: string;
|
||||
unresolved?: boolean;
|
||||
};
|
||||
IncludeStep: {
|
||||
sourceFile?: string;
|
||||
/** Format: int32 */
|
||||
lineNo?: number;
|
||||
};
|
||||
IngestSummary: {
|
||||
/** Format: int32 */
|
||||
ingested?: number;
|
||||
@@ -2223,6 +2685,8 @@ export interface components {
|
||||
InheritedFunction: {
|
||||
name?: string;
|
||||
declaredIn?: string;
|
||||
sourceFile?: string;
|
||||
viaCopycode?: boolean;
|
||||
/** Format: int32 */
|
||||
startLine?: number;
|
||||
/** Format: int32 */
|
||||
@@ -2296,8 +2760,12 @@ export interface components {
|
||||
ingestStatus?: string;
|
||||
ingestDepth?: string;
|
||||
};
|
||||
NextAction: {
|
||||
method?: string;
|
||||
path?: string;
|
||||
};
|
||||
/** @enum {string} */
|
||||
NodeType: "MODULE" | "FUNCTION" | "VARIABLE" | "DATA_STRUCTURE" | "DB_TABLE" | "CONSTANT" | "FIELD" | "DB_ACCESS" | "CONTROL_FLOW" | "PAYLOAD_FIELD";
|
||||
NodeType: "MODULE" | "FUNCTION" | "VARIABLE" | "DATA_STRUCTURE" | "DB_TABLE" | "CONSTANT" | "FIELD" | "DB_ACCESS" | "WORKFILE" | "WORKFILE_ACCESS" | "CONTROL_FLOW" | "PAYLOAD_FIELD";
|
||||
PayloadField: {
|
||||
tag?: string;
|
||||
field?: string;
|
||||
@@ -2341,6 +2809,10 @@ export interface components {
|
||||
generatedDir?: string;
|
||||
userExitDir?: string;
|
||||
};
|
||||
ResetResult: {
|
||||
/** Format: int32 */
|
||||
removed?: number;
|
||||
};
|
||||
/** @description A regex match in a module's source: module, file, line number, line text. */
|
||||
SourceMatch: {
|
||||
module?: string;
|
||||
@@ -2373,6 +2845,8 @@ export interface components {
|
||||
/** Format: int32 */
|
||||
endLine?: number;
|
||||
via?: string;
|
||||
sourceFile?: string;
|
||||
viaCopycode?: boolean;
|
||||
};
|
||||
SqlStatementSummary: {
|
||||
/** Format: int32 */
|
||||
@@ -2390,6 +2864,13 @@ export interface components {
|
||||
maxNodes?: number;
|
||||
hint?: string;
|
||||
};
|
||||
UnresolvedDynamicCall: {
|
||||
module?: string;
|
||||
originFile?: string;
|
||||
/** Format: int32 */
|
||||
lineNo?: number;
|
||||
variable?: string;
|
||||
};
|
||||
ValueMatch: {
|
||||
kind?: string;
|
||||
name?: string;
|
||||
@@ -2435,6 +2916,14 @@ export interface components {
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
WorkfileAccess: {
|
||||
workFile?: string;
|
||||
physicalName?: string;
|
||||
mode?: string;
|
||||
recordBuffers?: string[];
|
||||
lineNos?: number[];
|
||||
sites?: components["schemas"]["AccessSite"][];
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {useState} from "react";
|
||||
import {
|
||||
isModuleUnavailable,
|
||||
useDataStructureFields,
|
||||
useDbAccesses,
|
||||
useDispatchTable,
|
||||
@@ -28,6 +29,12 @@ interface DossierProps extends Props {
|
||||
* port a Natural module to Java. Read-only consumer of existing endpoints.
|
||||
*/
|
||||
export function MigrationDossier({project, moduleName, moduleSourceFile, onOpenLine, onOpenFileLine}: DossierProps) {
|
||||
// Item 107: every section below queries a different module endpoint, so an unknown or un-ingested
|
||||
// module used to paint five independent "failed to load" errors. ModuleView already shows one
|
||||
// banner for that case — render nothing here rather than repeating it five times. Any other
|
||||
// failure still surfaces per section, since it can affect one section and not the others.
|
||||
const {error} = useModulePayload(project, moduleName);
|
||||
if (isModuleUnavailable(error)) return null;
|
||||
return (
|
||||
<div className="space-y-6 p-4 text-sm">
|
||||
<PayloadSection project={project} moduleName={moduleName} onOpenFileLine={onOpenFileLine}/>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {useModuleContext} from "../api/hooks";
|
||||
import {isModuleUnavailable, useModuleContext} from "../api/hooks";
|
||||
import type {ModuleInfo} from "../api/client";
|
||||
import {ModuleNotes} from "./ModuleNotes";
|
||||
|
||||
/** Overview tab: read-only summary from GET /context (description, functions, calls, DB tables). */
|
||||
export function ModuleOverview({project, module}: { project: string; module: ModuleInfo }) {
|
||||
const {data, isLoading, isError} = useModuleContext(project, module.name);
|
||||
const {data, isLoading, isError, error} = useModuleContext(project, module.name);
|
||||
// Item 107: an unknown or un-ingested module is reported once by ModuleView's banner — don't
|
||||
// repeat it here as a generic failure, which would read as "the request broke".
|
||||
const unavailable = isModuleUnavailable(error);
|
||||
|
||||
const callees = data?.callees?.items?.map((i) => i.name).filter(Boolean) ?? [];
|
||||
const callers = data?.callers?.items?.map((i) => i.name).filter(Boolean) ?? [];
|
||||
@@ -14,7 +17,7 @@ export function ModuleOverview({project, module}: { project: string; module: Mod
|
||||
return (
|
||||
<div className="space-y-4 p-4 text-sm">
|
||||
{isLoading && <p className="text-neutral-400">loading context…</p>}
|
||||
{isError && <p className="text-red-500">failed to load context</p>}
|
||||
{isError && !unavailable && <p className="text-red-500">failed to load context</p>}
|
||||
{data && (
|
||||
<>
|
||||
{data.description && <p className="text-neutral-600 dark:text-neutral-300">{data.description}</p>}
|
||||
|
||||
41
ac-ui/src/components/ModuleUnavailable.tsx
Normal file
41
ac-ui/src/components/ModuleUnavailable.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {MODULE_NOT_INGESTED} from "../api/hooks";
|
||||
|
||||
/**
|
||||
* Item 107: the honest empty state for a module the server cannot analyse.
|
||||
*
|
||||
* <p>The API used to answer `200` with an all-zeros body for both a misspelt name and a module whose
|
||||
* source was never parsed, so every panel rendered "none" — indistinguishable from a module that
|
||||
* genuinely has no DB access, no callees and no data structures. It now answers `404` / `409`, and
|
||||
* this banner says which, once, instead of each panel independently reporting a failure.
|
||||
*
|
||||
* <p>`NOT_INGESTED` is not an error: the module is real and something calls it, its source just is
|
||||
* not in the graph. Its callers are still knowable, which is why the Calls and Graph tabs keep
|
||||
* working and the copy points there.
|
||||
*/
|
||||
export function ModuleUnavailable({moduleName, error}: { moduleName: string; error: unknown }) {
|
||||
const notIngested = error instanceof Error && error.message === MODULE_NOT_INGESTED;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"rounded border px-3 py-2 text-xs " +
|
||||
(notIngested
|
||||
? "border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200"
|
||||
: "border-red-300 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-200")
|
||||
}
|
||||
>
|
||||
{notIngested ? (
|
||||
<>
|
||||
<span className="font-semibold">{moduleName} is not ingested.</span>{" "}
|
||||
Something in this project calls it, but its source is not in the graph — so there is
|
||||
nothing to analyse here, rather than nothing to find. Its callers are still accurate:
|
||||
see the Calls and Graph tabs.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-semibold">No module {moduleName} in this project.</span>{" "}
|
||||
The name is unknown to the graph.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {lazy, Suspense, useState} from "react";
|
||||
import type {ModuleInfo} from "../api/client";
|
||||
import {useIngestNeighborhood, useRefreshModule} from "../api/hooks";
|
||||
import {isModuleUnavailable, useIngestNeighborhood, useModuleContext, useRefreshModule} from "../api/hooks";
|
||||
import {StatusBadge} from "./StatusBadge";
|
||||
import {ModuleOverview} from "./ModuleOverview";
|
||||
import {SourceView} from "./SourceView";
|
||||
@@ -11,6 +11,7 @@ import {MigrationDossier} from "./MigrationDossier";
|
||||
import {DataFlowView} from "./DataFlowView";
|
||||
import {ImpactView} from "./ImpactView";
|
||||
import {SourceOutline} from "./SourceOutline";
|
||||
import {ModuleUnavailable} from "./ModuleUnavailable";
|
||||
|
||||
// Lazy-loaded (M2): keeps Sigma/WebGL out of the initial bundle.
|
||||
const GraphView = lazy(() => import("./GraphView"));
|
||||
@@ -69,6 +70,9 @@ export function ModuleView({
|
||||
const refresh = useRefreshModule(project);
|
||||
const ingestNeighborhood = useIngestNeighborhood(project);
|
||||
const [identify, setIdentify] = useState<{ name: string; line: number } | null>(null);
|
||||
// Item 107: /context stands in for "can this module be analysed at all" — it is the broadest
|
||||
// module endpoint and the one the Overview tab already fetches, so this costs no extra request.
|
||||
const {error: contextError} = useModuleContext(project, module.name);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full flex-col border-l border-neutral-200 dark:border-neutral-800">
|
||||
@@ -118,6 +122,19 @@ export function ModuleView({
|
||||
</nav>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
{/*
|
||||
Item 107: one banner for the whole view when the server says this module cannot be
|
||||
analysed (404) or was never ingested (409), instead of every tab and every dossier
|
||||
section independently reporting a failure. Rendered above the tab body rather than
|
||||
in place of it: for a placeholder the Calls and Graph tabs still hold real data,
|
||||
because those come from the CALLING modules' source.
|
||||
useModuleContext shares React Query's cache with ModuleOverview — no extra request.
|
||||
*/}
|
||||
{isModuleUnavailable(contextError) && (
|
||||
<div className="p-4 pb-0">
|
||||
<ModuleUnavailable moduleName={module.name!} error={contextError}/>
|
||||
</div>
|
||||
)}
|
||||
{tab === "overview" && <ModuleOverview project={project} module={module}/>}
|
||||
{tab === "source" && (
|
||||
<div className="flex h-full min-h-0">
|
||||
|
||||
@@ -36,11 +36,23 @@ below. Nothing else is in your write scope.
|
||||
- `400 MISSING_LINE_RANGE` — `startLine`/`endLine` missing on `modules/{name}/source`.
|
||||
- `400 NO_SOURCE_FILE` — `/source` on an unresolved placeholder node.
|
||||
- `404 NODE_NOT_FOUND` / `404 MODULE_NOT_FOUND` — unknown id / module name.
|
||||
- `409 NOT_DEEPLY_INGESTED` / `409 NOT_INGESTED` — field-level dataflow
|
||||
endpoints only (`flow-forward`/`flow-backward`/`field-flow`): the module
|
||||
needs a per-module deep ingest (`nextAction` names the endpoint). That's
|
||||
**out of your read-only scope** — report it, don't call it. Not the same
|
||||
as "no data": the data may exist once deep-ingested.
|
||||
Every `/modules/{name}/…` endpoint checks this: an unknown module name is a
|
||||
`404`, never an empty `200`.
|
||||
- `409 NOT_DEEPLY_INGESTED` / `409 NOT_INGESTED` — the module needs a
|
||||
per-module deep ingest (`nextAction` names the endpoint). That's **out of
|
||||
your read-only scope** — report it, don't call it. Not the same as "no
|
||||
data": the data may exist once ingested. Two situations produce it:
|
||||
- field-level dataflow endpoints (`flow-forward`/`flow-backward`/`field-flow`)
|
||||
on a module that is only call-graph-ingested;
|
||||
- **any** `/modules/{name}/…` endpoint on an *unresolved placeholder* — a
|
||||
module something calls but whose source was never parsed. Its
|
||||
`callers` and `graph` still answer `200` with real data (that comes
|
||||
from the calling modules), so **fall back to `/callers`** to learn
|
||||
what you can. Everything else about it is unknowable, not empty.
|
||||
- **Never read an empty `200` from a module endpoint as "analysed, nothing
|
||||
found".** Since item 107 the API distinguishes *unknown* (`404`), *not
|
||||
analysable* (`409`) and *analysed, genuinely empty* (`200` with an empty
|
||||
body). Only the last one licenses the conclusion "there is nothing here".
|
||||
- **`null` means "not determined," not an error** — `dataType`, `value`,
|
||||
`table`, `view`, `module`, `description` are nullable by design.
|
||||
- **Don't fabricate endpoints.** Only what's listed below exists.
|
||||
|
||||
@@ -94,6 +94,38 @@ node's properties — so an agent can tell a dangling/dynamic
|
||||
reference apart from a resolved one. In `callers`/`callees` such targets already appear as entries
|
||||
with a blank `sourceFile`.
|
||||
|
||||
### Querying a module endpoint: three answers, not one (item 107)
|
||||
|
||||
Every `GET /api/projects/{p}/modules/{name}/…` endpoint used to answer `200` with an all-zeros shell
|
||||
for a name that exists nowhere in the graph, byte-identical to a real-but-empty module's answer. That
|
||||
is not cosmetic: `call-tree?depth=4` returning `200` with 0 modules reads as **"analysed, nothing
|
||||
found"** when the truth is **"not analysable"** — the module's source was never in the checkout. The
|
||||
graph knows three states and each now gets its own status:
|
||||
|
||||
| state | answer | what it means |
|
||||
|-------------------------------------------------------------------------------|---------------------------------------------------------------|------------------------------------------------------------------------|
|
||||
| no `MODULE` node for the name | `404 MODULE_NOT_FOUND` | unknown name (typo, or not in this project) |
|
||||
| **placeholder** — node exists because something calls it, source never parsed | `409` + `{status:"NOT_INGESTED", module, detail, nextAction}` | knowable in principle, not analysed yet |
|
||||
| real, ingested module | `200` | the body is the answer — an empty body genuinely means "nothing found" |
|
||||
|
||||
The `409` applies to everything derived from the module's **own** source: `digest`, `context`,
|
||||
`call-tree`, `callees`, `db-accesses`, `workfile-accesses`, `sql-statements`, `functions`,
|
||||
`functions/overrides`, `functions/{fn}/overrides`, `functions/{fn}/callers`, `data-structures`,
|
||||
`dispatch-table`, `payload`, `columns`.
|
||||
|
||||
`callers` and `graph` stay `200` for a placeholder — their data comes from the **calling** modules'
|
||||
source and is genuine. When you get a `409`, **fall back to `/callers`**: it is the one honest answer
|
||||
available for a module whose own source is missing. (`digest` no longer surfaces those callers, since
|
||||
its other fields would all be structurally zero.)
|
||||
|
||||
`/modules/{name}/source` is unchanged: it already answered `404 MODULE_NOT_FOUND` for both an absent
|
||||
module and a placeholder, since there is no source to serve either way.
|
||||
|
||||
**Scope limit — this guards the *root* module of a request only.** A `call-tree` that traverses *into*
|
||||
placeholder targets still reports that subtree as empty without flagging it, so a dispatcher whose
|
||||
targets are all placeholders still returns `200` with a silently truncated tree. Cross-check the
|
||||
targets you care about individually (a `409` tells you it is unanalysed) — see item 103.
|
||||
|
||||
**Data literals are not call targets (item 62).** A `CALLNAT <bareword>` whose target is really a data
|
||||
value — a browse key reaching the call site through a copycode/macro argument — used to leave a
|
||||
permanent unresolved placeholder callee. Enrichment now reaps such a placeholder when it has no Natural
|
||||
|
||||
@@ -77,9 +77,9 @@ before. The parked "parallel parse phase" idea was implemented 2026-07-18 (item
|
||||
|
||||
## Known bugs
|
||||
|
||||
- [ ] **107. Every module endpoint answers `200` with an empty shell for a module that does not exist —
|
||||
- [x] **107. Every module endpoint answers `200` with an empty shell for a module that does not exist —
|
||||
indistinguishable from a real but empty module** (found 2026-08-02, `upms` webservice-layer audit;
|
||||
contradicts the documented `404 MODULE_NOT_FOUND`)
|
||||
contradicts the documented `404 MODULE_NOT_FOUND`) — **done 2026-08-05**
|
||||
|
||||
**Symptom.** Measured against the live server, `upms`:
|
||||
```
|
||||
@@ -103,13 +103,37 @@ before. The parked "parallel parse phase" idea was implemented 2026-07-18 (item
|
||||
that trusts the `200` concludes "these paths trigger no commission processing"; the honest answer is
|
||||
"unknown". Same failure mode as item 103: an incomplete answer that looks complete.
|
||||
|
||||
**Fix.** Return `404 MODULE_NOT_FOUND` when no `MODULE` node exists for `(project, name)` — the check
|
||||
`search/identifier` already performs. If a node exists but its source is not ingested (placeholder,
|
||||
`sourceFile = ""`), that is a *different* state and deserves an explicit marker in the payload
|
||||
(`placeholder: true` / `sourceFile: null`) rather than an all-zeros body. Worth checking which other
|
||||
`/modules/{name}/…` endpoints share the shell (`functions`, `db-accesses`, `data-structures`,
|
||||
`dispatch-table` all plausibly do — `dispatch-table` returning `[]` for a non-existent module is
|
||||
currently indistinguishable from item 108's real gap).
|
||||
**Fix (as implemented 2026-08-05).** `ModuleIngestState` now carries `sourceFile`, so the three graph
|
||||
states are separable: `present()` (any node), `placeholder()` (node exists, `sourceFile = ""`),
|
||||
`ingested()` (real parsed node; the old `exists()`, renamed). Two guards in `AnalysisResource`
|
||||
replace the project-only `withProject` on every `/modules/{name}/…` endpoint:
|
||||
|
||||
- `withModule` → `404 MODULE_NOT_FOUND` when no node exists at all.
|
||||
- `withIngestedModule` → the same `404`, plus `409 {status:"NOT_INGESTED", module, detail, nextAction}`
|
||||
for a placeholder, reusing the existing `DeepIngestRequired` record.
|
||||
|
||||
Applied to all 15 endpoints whose answer comes from the module's own source (`digest`, `context`,
|
||||
`call-tree`, `callees`, `db-accesses`, `workfile-accesses`, `sql-statements`, `functions`,
|
||||
`functions/overrides`, `functions/{fn}/overrides`, `functions/{fn}/callers`, `data-structures`,
|
||||
`dispatch-table`, `payload`, `columns`). `callers` and `graph` get the `404` but stay `200` for a
|
||||
placeholder — their data comes from the *calling* modules and is genuine, so the `409` copy points
|
||||
callers there. `/modules/{name}/source` was already correct. Covered by `ModuleNotFoundIT` (54 cases,
|
||||
including an explicit assertion that the fixture really produces all three states). `ac-ui` maps both
|
||||
statuses to one banner instead of ~8 per-panel failures; the CLI needed no change (`printResponse`
|
||||
already prints any body and exits 1).
|
||||
|
||||
**Deviation from the fix sketched above.** The placeholder case returns `409` rather than a
|
||||
`placeholder: true` / `sourceFile: null` payload marker. A marker cannot be attached to the
|
||||
array-shaped responses (`db-accesses`, `functions`, `dispatch-table`, …), so the marker approach
|
||||
would have fixed the object-shaped endpoints only and left the rest indistinguishable — exactly the
|
||||
gap this item is about. The `409` is uniform, needs no DTO changes, and carries an actionable
|
||||
`nextAction`.
|
||||
|
||||
**Scope limit — the class of bug is NOT closed.** This guards only the **root** module of a request.
|
||||
A `call-tree` that traverses *into* placeholder targets still reports that subtree as empty without
|
||||
flagging it, so the original `WPOLIX0S`-style dispatcher whose 13 targets are all placeholders still
|
||||
returns `200` with a silently truncated tree. Callers must still cross-check individual targets (each
|
||||
now answers `409`). Flagging unanalysable nodes *inside* a traversal result is item 103's territory.
|
||||
|
||||
- [ ] **75. `CONTAINS` is not acyclic — 22 self-loops and 162 two-cycles in `upms`**
|
||||
(found 2026-07-17 while root-causing item 74; **cause NOT established — do not treat the notes below as
|
||||
|
||||
Reference in New Issue
Block a user