Roadmap
This commit is contained in:
@@ -358,6 +358,7 @@ agenticcode> exit
|
|||||||
ac project create upms /path/to/sources -l natural -d "UPMS legacy"
|
ac project create upms /path/to/sources -l natural -d "UPMS legacy"
|
||||||
ac project list
|
ac project list
|
||||||
ac project update upms -d "New description"
|
ac project update upms -d "New description"
|
||||||
|
ac project rename upms upms_alt # every node, override and counterpart reference follows
|
||||||
ac project delete upms
|
ac project delete upms
|
||||||
|
|
||||||
# Ingest / refresh
|
# Ingest / refresh
|
||||||
|
|||||||
@@ -12,17 +12,18 @@ import java.util.Objects;
|
|||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups project management subcommands: create, update, delete, recreate, show, list.
|
* Groups project management subcommands: create, update, delete, recreate, rename, show, list.
|
||||||
*/
|
*/
|
||||||
@Command(
|
@Command(
|
||||||
name = "project",
|
name = "project",
|
||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
description = "Create, update, delete, recreate, show or list projects",
|
description = "Create, update, delete, recreate, rename, show or list projects",
|
||||||
subcommands = {
|
subcommands = {
|
||||||
ProjectCommand.CreateCommand.class,
|
ProjectCommand.CreateCommand.class,
|
||||||
ProjectCommand.UpdateCommand.class,
|
ProjectCommand.UpdateCommand.class,
|
||||||
ProjectCommand.DeleteCommand.class,
|
ProjectCommand.DeleteCommand.class,
|
||||||
ProjectCommand.RecreateCommand.class,
|
ProjectCommand.RecreateCommand.class,
|
||||||
|
ProjectCommand.RenameCommand.class,
|
||||||
ProjectCommand.ShowCommand.class,
|
ProjectCommand.ShowCommand.class,
|
||||||
ProjectCommand.ListCommand.class
|
ProjectCommand.ListCommand.class
|
||||||
}
|
}
|
||||||
@@ -180,6 +181,28 @@ final class ProjectCommand implements Callable<Integer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item 202: rename a project in place (nodes, overrides and other projects' counterparts follow).
|
||||||
|
*/
|
||||||
|
@Command(name = "rename", mixinStandardHelpOptions = true,
|
||||||
|
description = "Rename a project; every node, override and counterpart reference follows")
|
||||||
|
static final class RenameCommand extends AbstractApiCommand {
|
||||||
|
|
||||||
|
@SuppressWarnings("NullAway.Init")
|
||||||
|
@Parameters(index = "0", description = "Current project name")
|
||||||
|
String name;
|
||||||
|
|
||||||
|
@SuppressWarnings("NullAway.Init")
|
||||||
|
@Parameters(index = "1", description = "New project name")
|
||||||
|
String newName;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer call() throws Exception {
|
||||||
|
return printResponse(apiClient().postJson("/api/projects/" + encode(name) + "/rename",
|
||||||
|
java.util.Map.of("newName", newName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item 78: re-initialise a project from its own stored config — no need to look up and re-type
|
* Item 78: re-initialise a project from its own stored config — no need to look up and re-type
|
||||||
* {@code root}/{@code language}/{@code generatedDir}/{@code userExitDir}, and no window in which they
|
* {@code root}/{@code language}/{@code generatedDir}/{@code userExitDir}, and no window in which they
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
server.url=http://localhost:8787
|
server.url=http://localhost:8787
|
||||||
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
|
# 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.
|
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
|
||||||
version=330
|
version=333
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.agenticcode.codeserver.api;
|
package com.agenticcode.codeserver.api;
|
||||||
|
|
||||||
import com.agenticcode.codeserver.service.ProjectIngestService;
|
import com.agenticcode.codeserver.service.ProjectIngestService;
|
||||||
|
import com.agenticcode.codeserver.service.ProjectMetadataCache;
|
||||||
import com.agenticcode.codeserver.service.ProjectRootResolver;
|
import com.agenticcode.codeserver.service.ProjectRootResolver;
|
||||||
import com.agenticcode.neo4jstore.graph.GraphRepository;
|
import com.agenticcode.neo4jstore.graph.GraphRepository;
|
||||||
import com.agenticcode.neo4jstore.graph.ProjectInfo;
|
import com.agenticcode.neo4jstore.graph.ProjectInfo;
|
||||||
@@ -38,14 +39,16 @@ public class ProjectResource {
|
|||||||
private final GraphRepository graphRepository;
|
private final GraphRepository graphRepository;
|
||||||
private final ProjectIngestService ingestService;
|
private final ProjectIngestService ingestService;
|
||||||
private final ProjectRootResolver rootResolver;
|
private final ProjectRootResolver rootResolver;
|
||||||
|
private final ProjectMetadataCache projectMetadataCache;
|
||||||
private final boolean scanOnCreate;
|
private final boolean scanOnCreate;
|
||||||
|
|
||||||
public ProjectResource(GraphRepository graphRepository, ProjectIngestService ingestService,
|
public ProjectResource(GraphRepository graphRepository, ProjectIngestService ingestService,
|
||||||
ProjectRootResolver rootResolver,
|
ProjectRootResolver rootResolver, ProjectMetadataCache projectMetadataCache,
|
||||||
@ConfigProperty(name = "agenticcode.tier1.scan-on-create", defaultValue = "true") boolean scanOnCreate) {
|
@ConfigProperty(name = "agenticcode.tier1.scan-on-create", defaultValue = "true") boolean scanOnCreate) {
|
||||||
this.graphRepository = graphRepository;
|
this.graphRepository = graphRepository;
|
||||||
this.ingestService = ingestService;
|
this.ingestService = ingestService;
|
||||||
this.rootResolver = rootResolver;
|
this.rootResolver = rootResolver;
|
||||||
|
this.projectMetadataCache = projectMetadataCache;
|
||||||
this.scanOnCreate = scanOnCreate;
|
this.scanOnCreate = scanOnCreate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +217,48 @@ public class ProjectResource {
|
|||||||
return graphRepository.clearAll().replaceWith(Response.noContent().build());
|
return graphRepository.clearAll().replaceWith(Response.noContent().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item 202: rename a project in place — every node, override and counterpart reference follows.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/{project}/rename")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(summary = "Rename a project (item 202)",
|
||||||
|
description = "Rewrites the project key on every node and override and in other projects' counterparts lists. "
|
||||||
|
+ "Batched; an interrupted rename is finished by re-running it.")
|
||||||
|
@APIResponse(responseCode = "200", description = "The renamed project.")
|
||||||
|
@APIResponse(responseCode = "400", description = "newName missing, blank or equal to the current name.",
|
||||||
|
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||||
|
@APIResponse(responseCode = "404", description = "Project not found.",
|
||||||
|
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||||
|
@APIResponse(responseCode = "409", description = "A project with the new name already exists.",
|
||||||
|
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||||
|
public Uni<Response> rename(@PathParam("project") String project, RenameRequest request) {
|
||||||
|
String newName = request == null || request.newName() == null ? "" : request.newName().trim();
|
||||||
|
if (newName.isEmpty() || newName.equals(project)) {
|
||||||
|
return Uni.createFrom().item(error(Response.Status.BAD_REQUEST, "INVALID_REQUEST",
|
||||||
|
"newName is required and must differ from the current name"));
|
||||||
|
}
|
||||||
|
return graphRepository.renameProject(project, newName)
|
||||||
|
.flatMap(result -> switch (result) {
|
||||||
|
case SUCCESS -> {
|
||||||
|
projectMetadataCache.invalidate(project);
|
||||||
|
projectMetadataCache.invalidate(newName);
|
||||||
|
yield graphRepository.getProject(newName).map(info -> Response.ok(info).build());
|
||||||
|
}
|
||||||
|
case NOT_FOUND -> Uni.createFrom().item(error(Response.Status.NOT_FOUND, "PROJECT_NOT_FOUND",
|
||||||
|
"Project '" + project + "' does not exist"));
|
||||||
|
case CONFLICT -> Uni.createFrom().item(error(Response.Status.CONFLICT, "PROJECT_EXISTS",
|
||||||
|
"Project '" + newName + "' already exists"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body of {@code POST /api/projects/{project}/rename}.
|
||||||
|
*/
|
||||||
|
public record RenameRequest(@Nullable String newName) {
|
||||||
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("/{project}")
|
@Path("/{project}")
|
||||||
@Operation(summary = "Delete a project")
|
@Operation(summary = "Delete a project")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ quarkus.http.port=8787
|
|||||||
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
|
# 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
|
# 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).
|
# info version (referenced below via property expression, not duplicated).
|
||||||
agenticcode.version=330
|
agenticcode.version=333
|
||||||
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
|
# 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.
|
# 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
|
mp.openapi.extensions.smallrye.info.title=AgenticCode API
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.agenticcode.codeserver.api;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item 202: renaming a project moves every node, its dynamic-call overrides and other projects'
|
||||||
|
* {@code counterparts} references; the old name answers 404 afterwards, the new one answers everything
|
||||||
|
* the old one did. Refuses a taken name and a blank or unchanged one.
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class ProjectRenameIT {
|
||||||
|
|
||||||
|
private static final String OLD = "rename-old";
|
||||||
|
private static final String NEW = "rename-new";
|
||||||
|
private static final String PEER = "rename-peer";
|
||||||
|
@TempDir
|
||||||
|
static Path root;
|
||||||
|
@TempDir
|
||||||
|
static Path peerRoot;
|
||||||
|
@jakarta.inject.Inject
|
||||||
|
org.neo4j.driver.Driver driver;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() {
|
||||||
|
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
|
||||||
|
write(root, "CALLER.nat", """
|
||||||
|
DEFINE DATA
|
||||||
|
LOCAL
|
||||||
|
01 #TGT (A8)
|
||||||
|
END-DEFINE
|
||||||
|
CALLNAT 'CALLEE' #TGT
|
||||||
|
CALLNAT #TGT
|
||||||
|
END
|
||||||
|
""");
|
||||||
|
write(root, "CALLEE.nat", """
|
||||||
|
DEFINE DATA
|
||||||
|
PARAMETER
|
||||||
|
01 #P (A8)
|
||||||
|
END-DEFINE
|
||||||
|
END
|
||||||
|
""");
|
||||||
|
write(peerRoot, "Peer.java", "package p; public class Peer {}\n");
|
||||||
|
given().contentType("application/json")
|
||||||
|
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natural", null, null))
|
||||||
|
.when().post("/api/projects/" + OLD).then().statusCode(201);
|
||||||
|
given().when().post("/api/projects/" + OLD + "/refresh?deep=true").then().statusCode(200).body("failed", empty());
|
||||||
|
given().contentType("application/json")
|
||||||
|
.body(new ProjectResource.ProjectRequest(null, peerRoot.toString(), null, "java", null, null, List.of(OLD)))
|
||||||
|
.when().post("/api/projects/" + PEER).then().statusCode(201);
|
||||||
|
// a manual override that must follow the rename
|
||||||
|
String file = given().when().get("/api/projects/" + OLD + "/dynamic-calls/unresolved").then().statusCode(200)
|
||||||
|
.extract().path("[0].originFile");
|
||||||
|
Integer line = given().when().get("/api/projects/" + OLD + "/dynamic-calls/unresolved").then().statusCode(200)
|
||||||
|
.extract().path("[0].lineNo");
|
||||||
|
given().contentType("application/json")
|
||||||
|
.body(Map.of("originFile", file, "lineNo", line, "targets", List.of("CALLEE"), "variable", "#TGT"))
|
||||||
|
.when().post("/api/projects/" + OLD + "/dynamic-calls/overrides").then().statusCode(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void write(Path dir, String fileName, String content) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve(fileName), content);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void renameMovesNodesOverridesAndCounterpartReferences() {
|
||||||
|
given().contentType("application/json").body(Map.of("newName", " "))
|
||||||
|
.when().post("/api/projects/" + OLD + "/rename").then().statusCode(400).body("code", equalTo("INVALID_REQUEST"));
|
||||||
|
given().contentType("application/json").body(Map.of("newName", OLD))
|
||||||
|
.when().post("/api/projects/" + OLD + "/rename").then().statusCode(400);
|
||||||
|
given().contentType("application/json").body(Map.of("newName", PEER))
|
||||||
|
.when().post("/api/projects/" + OLD + "/rename").then().statusCode(409).body("code", equalTo("PROJECT_EXISTS"));
|
||||||
|
given().contentType("application/json").body(Map.of("newName", NEW))
|
||||||
|
.when().post("/api/projects/nope/rename").then().statusCode(404);
|
||||||
|
|
||||||
|
given().contentType("application/json").body(Map.of("newName", NEW))
|
||||||
|
.when().post("/api/projects/" + OLD + "/rename").then().statusCode(200)
|
||||||
|
.body("name", equalTo(NEW)).body("language", equalTo("natural")).body("ingest.filesPersisted", equalTo(2));
|
||||||
|
|
||||||
|
given().when().get("/api/projects/" + OLD).then().statusCode(404);
|
||||||
|
given().when().get("/api/projects/" + NEW + "/modules").then().statusCode(200).body("name", containsInAnyOrder("CALLER", "CALLEE"));
|
||||||
|
given().when().get("/api/projects/" + NEW + "/modules/CALLER/callees").then().statusCode(200)
|
||||||
|
.body("items.name", hasItem("CALLEE"));
|
||||||
|
given().when().get("/api/projects/" + NEW + "/dynamic-calls/overrides").then().statusCode(200)
|
||||||
|
.body("size()", equalTo(1)).body("[0].targets", contains("CALLEE"));
|
||||||
|
given().when().get("/api/projects/" + PEER).then().statusCode(200).body("counterparts", contains(NEW));
|
||||||
|
given().when().get("/api/projects").then().statusCode(200)
|
||||||
|
.body("name", hasItem(NEW)).body("name", not(hasItem(OLD)));
|
||||||
|
|
||||||
|
// a full delete takes the project's overrides with it (found on upms2: 10 orphaned override nodes)
|
||||||
|
given().when().delete("/api/projects/" + NEW).then().statusCode(204);
|
||||||
|
try (var session = driver.session()) {
|
||||||
|
long orphans = session.run("MATCH (o:DynamicCallOverride {project: $p}) RETURN count(o) AS c", Map.of("p", NEW))
|
||||||
|
.single().get("c").asLong();
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(0, orphans, "override nodes of a deleted project");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2798,6 +2798,38 @@ public final class CypherQueries {
|
|||||||
* graph. Split from the node delete so {@code recreate} can reuse the latter alone. Not batched: it
|
* graph. Split from the node delete so {@code recreate} can reuse the latter alone. Not batched: it
|
||||||
* is a single node.
|
* is a single node.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Item 202: renames a project. The name is the key on every {@code AstNode} ({@code project}), on the
|
||||||
|
* {@code DynamicCallOverride}s and on the {@code Project} shell, and other shells may list it in
|
||||||
|
* {@code counterparts}; edges carry no project. The node rewrite is batched like
|
||||||
|
* {@link #DELETE_PROJECT_NODES} (implicit transaction) and runs first, so an interrupted rename is
|
||||||
|
* finished by re-running it: nodes already moved answer to the new name, the shell still to the old.
|
||||||
|
*/
|
||||||
|
public static final String RENAME_PROJECT_NODES = """
|
||||||
|
MATCH (n:AstNode {project: $from})
|
||||||
|
CALL { WITH n SET n.project = $to } IN TRANSACTIONS OF $batchSize ROWS
|
||||||
|
""";
|
||||||
|
public static final String RENAME_PROJECT_OVERRIDES = """
|
||||||
|
MATCH (o:DynamicCallOverride {project: $from})
|
||||||
|
SET o.project = $to
|
||||||
|
""";
|
||||||
|
public static final String RENAME_PROJECT_COUNTERPART_REFS = """
|
||||||
|
MATCH (p:Project)
|
||||||
|
WHERE $from IN coalesce(p.counterparts, [])
|
||||||
|
SET p.counterparts = [c IN p.counterparts | CASE WHEN c = $from THEN $to ELSE c END]
|
||||||
|
""";
|
||||||
|
public static final String RENAME_PROJECT_SHELL = """
|
||||||
|
MATCH (p:Project {name: $from})
|
||||||
|
SET p.name = $to
|
||||||
|
""";
|
||||||
|
/**
|
||||||
|
* Item 202 (found while deleting {@code upms2}): a project's manual overrides are its own data and
|
||||||
|
* go with the full delete. The recreate path (item 78) keeps them on purpose — they are config.
|
||||||
|
*/
|
||||||
|
public static final String DELETE_PROJECT_OVERRIDES = """
|
||||||
|
MATCH (o:DynamicCallOverride {project: $name})
|
||||||
|
DELETE o
|
||||||
|
""";
|
||||||
public static final String DELETE_PROJECT_SHELL = """
|
public static final String DELETE_PROJECT_SHELL = """
|
||||||
MATCH (p:Project {name: $name})
|
MATCH (p:Project {name: $name})
|
||||||
DELETE p
|
DELETE p
|
||||||
|
|||||||
@@ -2441,12 +2441,39 @@ public class GraphRepository {
|
|||||||
return ProjectOpResult.NOT_FOUND;
|
return ProjectOpResult.NOT_FOUND;
|
||||||
}
|
}
|
||||||
deleteNodesBatched(session, name);
|
deleteNodesBatched(session, name);
|
||||||
|
session.run(CypherQueries.DELETE_PROJECT_OVERRIDES, Map.of("name", name)).consume();
|
||||||
session.run(CypherQueries.DELETE_PROJECT_SHELL, Map.of("name", name)).consume();
|
session.run(CypherQueries.DELETE_PROJECT_SHELL, Map.of("name", name)).consume();
|
||||||
return ProjectOpResult.SUCCESS;
|
return ProjectOpResult.SUCCESS;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item 202: renames {@code from} to {@code to} — nodes (batched, implicit transaction like the
|
||||||
|
* delete), overrides, other projects' {@code counterparts} entries, then the shell.
|
||||||
|
*
|
||||||
|
* @return {@code NOT_FOUND} if {@code from} does not exist, {@code CONFLICT} if {@code to} already
|
||||||
|
* does, else {@code SUCCESS}.
|
||||||
|
*/
|
||||||
|
public Uni<ProjectOpResult> renameProject(String from, String to) {
|
||||||
|
return Uni.createFrom().item(() -> {
|
||||||
|
try (Session session = driver.session()) {
|
||||||
|
if (!session.run(CypherQueries.PROJECT_EXISTS, Map.of("name", from)).hasNext()) {
|
||||||
|
return ProjectOpResult.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (session.run(CypherQueries.PROJECT_EXISTS, Map.of("name", to)).hasNext()) {
|
||||||
|
return ProjectOpResult.CONFLICT;
|
||||||
|
}
|
||||||
|
Map<String, Object> params = Map.of("from", from, "to", to, "batchSize", deleteBatchSize);
|
||||||
|
session.run(CypherQueries.RENAME_PROJECT_NODES, params).consume();
|
||||||
|
session.run(CypherQueries.RENAME_PROJECT_OVERRIDES, params).consume();
|
||||||
|
session.run(CypherQueries.RENAME_PROJECT_COUNTERPART_REFS, params).consume();
|
||||||
|
session.run(CypherQueries.RENAME_PROJECT_SHELL, params).consume();
|
||||||
|
return ProjectOpResult.SUCCESS;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item 78: deletes a project's graph but keeps its {@code (:Project)} shell, so the config survives.
|
* Item 78: deletes a project's graph but keeps its {@code (:Project)} shell, so the config survives.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -314,6 +314,17 @@ already ran are committed, so queries keep answering, just from a half-updated g
|
|||||||
self-heal (a killed process clears nothing) and does not distinguish "running right now" from "died an
|
self-heal (a killed process clears nothing) and does not distinguish "running right now" from "died an
|
||||||
hour ago" — both mean the same thing to a caller. A completed refresh clears it.
|
hour ago" — both mean the same thing to a caller. A completed refresh clears it.
|
||||||
|
|
||||||
|
## Renaming a project (item 202)
|
||||||
|
|
||||||
|
`POST /api/projects/{p}/rename` with `{"newName": "…"}` (CLI `ac project rename <old> <new>`)
|
||||||
|
moves the project key on every node and override and in other projects' `counterparts` lists, then
|
||||||
|
the shell; the old name answers `404` afterwards and nothing needs re-ingesting. Refusals:
|
||||||
|
`400 INVALID_REQUEST` (blank or unchanged), `404`, `409 PROJECT_EXISTS`. The node rewrite is batched;
|
||||||
|
an interrupted rename is finished by running it again (97 s for the 939k-node `upms`). Use it to
|
||||||
|
keep a reference graph next to a fresh ingest (`ac project rename upms upms_alt`, then create `upms`
|
||||||
|
again and compare). A full `DELETE` of a project now also removes its manual overrides; `recreate`
|
||||||
|
keeps them.
|
||||||
|
|
||||||
## Is this project's graph any good? (item 126)
|
## Is this project's graph any good? (item 126)
|
||||||
|
|
||||||
`GET /api/projects` and `GET /api/projects/{p}` (CLI `ac project list` / `ac project show <p>`)
|
`GET /api/projects` and `GET /api/projects/{p}` (CLI `ac project list` / `ac project show <p>`)
|
||||||
|
|||||||
@@ -5505,6 +5505,26 @@ reproduced the bug.)*
|
|||||||
75 reading tokens), `palette.primary.dark` read 26 times; no placeholder left except the undeclared
|
75 reading tokens), `palette.primary.dark` read 26 times; no placeholder left except the undeclared
|
||||||
tokens. The item-195 inherited-field fix is confirmed on the same run (its 8 placeholders are gone).
|
tokens. The item-195 inherited-field fix is confirmed on the same run (its 8 placeholders are gone).
|
||||||
|
|
||||||
|
## Project rename — item 202 (2026-09-23)
|
||||||
|
|
||||||
|
- [x] **202. A project cannot be renamed**
|
||||||
|
|
||||||
|
`POST /api/projects/{name}/rename` with `{"newName": "..."}` (CLI `ac project rename <old> <new>`)
|
||||||
|
rewrites the project key everywhere it lives: `project` on every `AstNode` (batched
|
||||||
|
`IN TRANSACTIONS` like the delete, implicit transaction), on the `DynamicCallOverride`s, the
|
||||||
|
entries of other projects' `counterparts` lists, and finally the `Project` shell's `name`. Edges
|
||||||
|
carry no project. `400 INVALID_REQUEST` for a blank or unchanged name, `404` for an unknown project,
|
||||||
|
`409 PROJECT_EXISTS` for a taken target. The metadata cache is invalidated for both names. Because
|
||||||
|
the nodes move first and the shell last, an interrupted rename is finished by re-running it (the
|
||||||
|
shell still answers to the old name until then). Test: `ProjectRenameIT` (nodes, callees, override
|
||||||
|
and a peer's counterpart reference follow; old name 404; the three refusals).
|
||||||
|
|
||||||
|
Found on the way: `DELETE /api/projects/{p}` left the project's `DynamicCallOverride` nodes behind
|
||||||
|
(10 orphans after deleting `upms2`). The full delete now removes them; the recreate path (item 78)
|
||||||
|
keeps them on purpose, they are configuration. Asserted at the end of `ProjectRenameIT`.
|
||||||
|
|
||||||
|
Measured: renaming `upms` (938 746 nodes, 10 overrides) to `upms_alt` took 97 s on server 333.
|
||||||
|
|
||||||
## Override apply rebuilds `CALLS_MODULE`; single-INCLUDE programs — items 200, 201 (2026-09-23)
|
## Override apply rebuilds `CALLS_MODULE`; single-INCLUDE programs — items 200, 201 (2026-09-23)
|
||||||
|
|
||||||
- [x] **200. A dynamic-`CALLNAT` override is applied to `CALLS` at once, but the derived `CALLS_MODULE` edges are not
|
- [x] **200. A dynamic-`CALLNAT` override is applied to `CALLS` at once, but the derived `CALLS_MODULE` edges are not
|
||||||
|
|||||||
@@ -106,6 +106,22 @@ Item **141** — the one that made the API return a *wrong* answer rather than a
|
|||||||
|
|
||||||
## Known bugs
|
## Known bugs
|
||||||
|
|
||||||
|
- [ ] **203. Synthetic inheritance `CALLS` edges carry an arbitrary line and are never reaped**
|
||||||
|
(found 2026-09-23 comparing a fresh `pur` ingest against the 2026-09-11 one)
|
||||||
|
|
||||||
|
**Symptom.** `LINK_CALLS_TO_IMPLEMENTATIONS` merges one `CALLS {resolvedVia: 'INHERITANCE'}` edge
|
||||||
|
per caller/implementation pair and sets `lineNo` `ON CREATE` from whichever originating base call
|
||||||
|
the match meets first. With several call sites the line is arbitrary and changes between runs:
|
||||||
|
`SearchResultLogic` -> `ResultRepository` was line 95 in one graph and 97 in the other, both real
|
||||||
|
calls. `callers`/`callees` sites for such an edge therefore point at one call chosen at random.
|
||||||
|
The edge also carries no `ingestGen`, so the item-198 reap never removes it when the base call is
|
||||||
|
gone; only a project recreate does (`pur`: 75 stale ones next to 235 new ones in unchanged files).
|
||||||
|
|
||||||
|
**Fix.** Key the synthetic edge on the originating call (`lineNo` + `originFile`, one edge per base
|
||||||
|
call site, like the parsed edge it derives from) or aggregate all originating lines into a list;
|
||||||
|
and delete the project's (or the scoped modules') inheritance edges before re-deriving them in the
|
||||||
|
finalize, as `DELETE_CALLS_MODULE` does for `CALLS_MODULE`.
|
||||||
|
|
||||||
- [ ] **152. `NaturalLines.stripInlineComment` is not quote-aware — a `/*` inside a string literal
|
- [ ] **152. `NaturalLines.stripInlineComment` is not quote-aware — a `/*` inside a string literal
|
||||||
truncates the statement** (split out of item 141 on 2026-08-28, pre-existing)
|
truncates the statement** (split out of item 141 on 2026-08-28, pre-existing)
|
||||||
|
|
||||||
@@ -260,20 +276,6 @@ wrong answer, found by the 2026-07-17 `VMULTMN4` audit.)*
|
|||||||
|
|
||||||
## Agent API gaps
|
## Agent API gaps
|
||||||
|
|
||||||
- [ ] **202. A project cannot be renamed** (asked 2026-09-23 while rebuilding `upms2` as a copy of `upms`)
|
|
||||||
|
|
||||||
**Symptom.** `PUT /api/projects/{name}` changes description, root, language, exclude dirs and
|
|
||||||
counterparts, but the name is the key: every node, edge and `DynamicCallOverride` carries a
|
|
||||||
`project` property, and the `Project` node is found by it. Renaming today means create + full
|
|
||||||
ingest + replaying the overrides + delete, i.e. ~7 minutes on `upms`.
|
|
||||||
|
|
||||||
**Fix.** `POST /api/projects/{name}/rename` (body `{newName}`) with a matching `ac project rename`,
|
|
||||||
refusing an existing target name and a name that is someone's counterpart unless those are rewritten
|
|
||||||
too: one transaction that sets `project` on the `Project` node, all `AstNode`s, all
|
|
||||||
`DynamicCallOverride`s and every edge property that stores it, then rewrites `counterparts` lists
|
|
||||||
that name the old project. Must be measured on `upms` (2.1 M nodes): a single `SET` over all nodes
|
|
||||||
may need batching (`CALL { } IN TRANSACTIONS`).
|
|
||||||
|
|
||||||
- [ ] **108. `dispatch-table` only understands the `DECIDE` dispatcher, not the dispatch-*table* idiom —
|
- [ ] **108. `dispatch-table` only understands the `DECIDE` dispatcher, not the dispatch-*table* idiom —
|
||||||
the one the endpoint is named after** (found 2026-08-02, `upms` webservice-layer audit)
|
the one the endpoint is named after** (found 2026-08-02, `upms` webservice-layer audit)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user