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 list
|
||||
ac project update upms -d "New description"
|
||||
ac project rename upms upms_alt # every node, override and counterpart reference follows
|
||||
ac project delete upms
|
||||
|
||||
# Ingest / refresh
|
||||
|
||||
@@ -12,17 +12,18 @@ import java.util.Objects;
|
||||
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(
|
||||
name = "project",
|
||||
mixinStandardHelpOptions = true,
|
||||
description = "Create, update, delete, recreate, show or list projects",
|
||||
description = "Create, update, delete, recreate, rename, show or list projects",
|
||||
subcommands = {
|
||||
ProjectCommand.CreateCommand.class,
|
||||
ProjectCommand.UpdateCommand.class,
|
||||
ProjectCommand.DeleteCommand.class,
|
||||
ProjectCommand.RecreateCommand.class,
|
||||
ProjectCommand.RenameCommand.class,
|
||||
ProjectCommand.ShowCommand.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
|
||||
* {@code root}/{@code language}/{@code generatedDir}/{@code userExitDir}, and no window in which they
|
||||
|
||||
@@ -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=330
|
||||
version=333
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.agenticcode.codeserver.api;
|
||||
|
||||
import com.agenticcode.codeserver.service.ProjectIngestService;
|
||||
import com.agenticcode.codeserver.service.ProjectMetadataCache;
|
||||
import com.agenticcode.codeserver.service.ProjectRootResolver;
|
||||
import com.agenticcode.neo4jstore.graph.GraphRepository;
|
||||
import com.agenticcode.neo4jstore.graph.ProjectInfo;
|
||||
@@ -38,14 +39,16 @@ public class ProjectResource {
|
||||
private final GraphRepository graphRepository;
|
||||
private final ProjectIngestService ingestService;
|
||||
private final ProjectRootResolver rootResolver;
|
||||
private final ProjectMetadataCache projectMetadataCache;
|
||||
private final boolean scanOnCreate;
|
||||
|
||||
public ProjectResource(GraphRepository graphRepository, ProjectIngestService ingestService,
|
||||
ProjectRootResolver rootResolver,
|
||||
ProjectRootResolver rootResolver, ProjectMetadataCache projectMetadataCache,
|
||||
@ConfigProperty(name = "agenticcode.tier1.scan-on-create", defaultValue = "true") boolean scanOnCreate) {
|
||||
this.graphRepository = graphRepository;
|
||||
this.ingestService = ingestService;
|
||||
this.rootResolver = rootResolver;
|
||||
this.projectMetadataCache = projectMetadataCache;
|
||||
this.scanOnCreate = scanOnCreate;
|
||||
}
|
||||
|
||||
@@ -214,6 +217,48 @@ public class ProjectResource {
|
||||
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
|
||||
@Path("/{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
|
||||
# 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=330
|
||||
agenticcode.version=333
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
* 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 = """
|
||||
MATCH (p:Project {name: $name})
|
||||
DELETE p
|
||||
|
||||
@@ -2441,12 +2441,39 @@ public class GraphRepository {
|
||||
return ProjectOpResult.NOT_FOUND;
|
||||
}
|
||||
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();
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
`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
|
||||
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)
|
||||
|
||||
- [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
|
||||
|
||||
- [ ] **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
|
||||
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
|
||||
|
||||
- [ ] **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 —
|
||||
the one the endpoint is named after** (found 2026-08-02, `upms` webservice-layer audit)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user