From b56cb592e9635604a036f434f28cba39f1d02ce4 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 4 Jul 2026 11:35:41 +0200 Subject: [PATCH 1/2] fix(graph-ui): label Project name honestly as permanent ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The New Index modal presented the project name as an optional display name, but the value becomes permanent identity: the database filename, the projects primary key, and the QN prefix of every node. It can never be renamed afterwards. Implements the decision from the #805 review: keep the field, make it honest. The label now reads "Project ID (optional — permanent, cannot be renamed)", the placeholder and a new help line explain that the ID becomes the database name and query prefix and that leaving it blank derives it from the path. Both EN and zh locales updated, test asserting the old label adjusted. Co-authored-by: rarepops Signed-off-by: Martin Vogel --- graph-ui/src/components/StatsTab.test.tsx | 2 +- graph-ui/src/components/StatsTab.tsx | 1 + graph-ui/src/lib/i18n.ts | 10 ++++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/graph-ui/src/components/StatsTab.test.tsx b/graph-ui/src/components/StatsTab.test.tsx index f0cc22d58..bc6d41fe8 100644 --- a/graph-ui/src/components/StatsTab.test.tsx +++ b/graph-ui/src/components/StatsTab.test.tsx @@ -65,7 +65,7 @@ describe("StatsTab index modal", () => { fireEvent.change(await screen.findByLabelText("Repository path"), { target: { value: "D:\\work\\信租风控通后端" }, }); - fireEvent.change(screen.getByLabelText("Project name"), { + fireEvent.change(screen.getByLabelText("Project ID (optional — permanent, cannot be renamed)"), { target: { value: "信租风控通后端" }, }); fireEvent.click(screen.getByRole("button", { name: "Index This Folder" })); diff --git a/graph-ui/src/components/StatsTab.tsx b/graph-ui/src/components/StatsTab.tsx index 07e15fd5a..67c38810f 100644 --- a/graph-ui/src/components/StatsTab.tsx +++ b/graph-ui/src/components/StatsTab.tsx @@ -269,6 +269,7 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat onChange={(e) => setProjectName(e.target.value)} className="w-full bg-white/[0.04] border border-white/[0.06] rounded-lg px-3 py-2 text-[12px] text-foreground outline-none focus:border-primary/40 placeholder:text-foreground/20" /> + {t.index.projectNameHelp} diff --git a/graph-ui/src/lib/i18n.ts b/graph-ui/src/lib/i18n.ts index 942d67ade..e98e1de4e 100644 --- a/graph-ui/src/lib/i18n.ts +++ b/graph-ui/src/lib/i18n.ts @@ -45,8 +45,9 @@ export const messages = { selectRepositoryFolder: "Select Repository Folder", instructions: "Navigate to the project root and click \"Index This Folder\".", repositoryPath: "Repository path", - projectName: "Project name", - projectNamePlaceholder: "Optional display name", + projectName: "Project ID (optional — permanent, cannot be renamed)", + projectNamePlaceholder: "Derived from folder name if blank", + projectNameHelp: "Becomes the database name and query prefix. Leave blank to derive it from the path.", filterFolders: "Filter folders", noSubdirectories: "No subdirectories", indexThisFolder: "Index This Folder", @@ -116,8 +117,9 @@ export const messages = { selectRepositoryFolder: "选择仓库目录", instructions: "导航到项目根目录,然后点击“索引此目录”。", repositoryPath: "仓库路径", - projectName: "项目名称", - projectNamePlaceholder: "可选显示名称", + projectName: "项目 ID(可选,永久且不可重命名)", + projectNamePlaceholder: "留空则从路径派生", + projectNameHelp: "将作为数据库名称与查询前缀;留空则从路径派生。", filterFolders: "筛选目录", noSubdirectories: "没有子目录", indexThisFolder: "索引此目录", From b58d1d84e61ccf5ca646c348a83ed6806bb39394 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 4 Jul 2026 13:37:45 +0200 Subject: [PATCH 2/2] =?UTF-8?q?test(repro):=20RED=20=E2=80=94=20TS=20cross?= =?UTF-8?q?-file=20inherited=20method=20call=20unresolved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #840 flips the lrp_ts_s6_inherited_method probe from a fragile false-green (a unique_name short-name registry guess that happened to bind the right node in a 2-file fixture) to asserting the weak edge is suppressed. That flip is correct, but it leaves the underlying gap — ts_lsp cross-file INHERITED method call resolution — with no red reproduction anywhere. Add tests/repro/repro_ts_inherited_method.c to the non-gating repro suite: base.ts defines Base.greet, derived.ts has Derived extends Base and a typed call site d.greet(). A GREEN precondition test proves both files extract cleanly, the Base.greet Method def exists and the call site is extracted; the RED pipeline test additionally verifies the callee node and the INHERITS edge are in the graph, then asserts the correct outcome: a CALLS edge callSite -> *.Base.greet carrying an lsp_ts_* strategy on that same edge. The pre-#840 lucky unique_name fallback edge does not satisfy it (verified: the run dumps that exact edge, strategy unique_name, confidence 0.75), and post-#840 suppression yields no edge at all — so the test stays RED until ts_lsp genuinely resolves cross-file inheritance. Guards the gap left visible after Signed-off-by: Martin Vogel #840's probe flip; see the test header for the root-cause pointer. --- Makefile.cbm | 1 + tests/repro/repro_main.c | 3 + tests/repro/repro_ts_inherited_method.c | 263 ++++++++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 tests/repro/repro_ts_inherited_method.c diff --git a/Makefile.cbm b/Makefile.cbm index 5bf14b3b9..da534cb0b 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -469,6 +469,7 @@ TEST_REPRO_SRCS = \ tests/repro/repro_lsp_c_cpp.c \ tests/repro/repro_lsp_go_py.c \ tests/repro/repro_lsp_ts.c \ + tests/repro/repro_ts_inherited_method.c \ tests/repro/repro_lsp_java_cs.c \ tests/repro/repro_lsp_kt_php_rust.c diff --git a/tests/repro/repro_main.c b/tests/repro/repro_main.c index 6c516be32..319a83465 100644 --- a/tests/repro/repro_main.c +++ b/tests/repro/repro_main.c @@ -105,6 +105,8 @@ extern void suite_repro_grammar_misc(void); extern void suite_repro_lsp_c_cpp(void); extern void suite_repro_lsp_go_py(void); extern void suite_repro_lsp_ts(void); +/* TS cross-file inherited-method resolution gap (post-#840 probe flip) */ +extern void suite_repro_ts_inherited_method(void); extern void suite_repro_lsp_java_cs(void); extern void suite_repro_lsp_kt_php_rust(void); @@ -172,6 +174,7 @@ int main(void) { RUN_SUITE(repro_lsp_c_cpp); RUN_SUITE(repro_lsp_go_py); RUN_SUITE(repro_lsp_ts); + RUN_SUITE(repro_ts_inherited_method); RUN_SUITE(repro_lsp_java_cs); RUN_SUITE(repro_lsp_kt_php_rust); diff --git a/tests/repro/repro_ts_inherited_method.c b/tests/repro/repro_ts_inherited_method.c new file mode 100644 index 000000000..fac58ef7f --- /dev/null +++ b/tests/repro/repro_ts_inherited_method.c @@ -0,0 +1,263 @@ +/* + * repro_ts_inherited_method.c — RED reproduction: TypeScript CROSS-FILE + * INHERITED method call resolution gap (ts_lsp). + * + * THE GAP: a typed call to a method the receiver's class INHERITS from a base + * class defined in ANOTHER file is never resolved by the TS LSP: + * + * base.ts: export class Base { greet(): string { ... } } + * derived.ts: import { Base } from "./base"; + * export class Derived extends Base { ... } + * export function callSite(): string { + * const d: Derived = new Derived(); + * return d.greet(); <-- inherited, cross-file + * } + * + * CORRECT behaviour (asserted here, so this test is RED until fixed): a CALLS + * edge from the caller (callable-sourced, QN contains "callSite") to the BASE + * method definition (target QN suffix ".Base.greet" — mirroring the + * java/kotlin/php inherited-dispatch convention: resolution lands on the base + * class's method def, since Derived declares no `greet` and no such node can + * exist), carrying a genuine TS-LSP resolution strategy ("lsp_ts_*" in the + * edge's properties_json, per the ts_emit_resolved_call contract documented in + * repro_lsp_ts.c). + * + * WHY the strategy tag is part of the invariant: before the weak-short-name + * suppression (PR #840, recovering withdrawn #836/#835), this scenario looked + * resolved via a unique_name REGISTRY fallback — "greet" is unique in a 2-file + * fixture, so a short-name guess happened to bind the right node (a false + * green; in a real repo the same guess binds an arbitrary same-named method). + * PR #840 flipped the probe lrp_ts_s6_inherited_method + * (tests/test_lsp_resolution_probe.c) to assert that this weak edge is + * SUPPRESSED (calls == 0) — correct, but it leaves the underlying resolution + * gap without any red reproduction. THIS test is that reproduction: + * - on pre-#840 code the lucky edge exists but carries NO "lsp_ts" strategy + * -> RED (the green was never genuine); + * - on post-#840 code the weak edge is suppressed, no edge exists at all + * -> RED; + * - only genuine ts_lsp cross-file inheritance resolution turns it GREEN. + * + * ROOT-CAUSE POINTER (for the eventual fixer): ts_lsp cross-file inheritance + * resolution — internal/cbm/lsp/ts_lsp.c resolve_member_call/lookup_method + * only walks methods declared on the receiver's OWN class as registered in the + * module registry; it does not follow the (correctly extracted) INHERITS edge + * from Derived to an imported Base to find `greet` there. See PR #836/#840 and + * the S6 probe lrp_ts_s6_inherited_method for the full analysis. The INHERITS + * edge and both defs ARE in the graph (asserted below as preconditions), so a + * red here is the RESOLUTION gap, not an extraction failure. + * + * NOTE: line comments only inside this header (no nested block comments, per + * coding rules). + */ + +#include "test_framework.h" +#include "repro_invariant_lib.h" +#include + +#include + +/* ── Fixture ─────────────────────────────────────────────────────────────── */ + +static const RFile kTsInherited[] = { + {"base.ts", + "export class Base {\n" + " greet(): string { return \"hello\"; }\n" + "}\n"}, + {"derived.ts", + "import { Base } from \"./base\";\n" + "\n" + "export class Derived extends Base {\n" + " extra(): number { return 2; }\n" + "}\n" + "\n" + "export function callSite(): string {\n" + " const d: Derived = new Derived();\n" + " return d.greet();\n" + "}\n"}, +}; + +/* ── Local store helpers ─────────────────────────────────────────────────── */ + +/* True if some node with `label` has a QN ending in `suffix`. */ +static int node_with_qn_suffix(cbm_store_t *store, const char *project, + const char *label, const char *suffix) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) != + CBM_STORE_OK) + return 0; + int found = 0; + size_t sl = strlen(suffix); + for (int i = 0; i < count && !found; i++) { + const char *qn = nodes[i].qualified_name; + if (qn) { + size_t ql = strlen(qn); + if (ql >= sl && strcmp(qn + ql - sl, suffix) == 0) + found = 1; + } + } + cbm_store_free_nodes(nodes, count); + return found; +} + +/* + * The PRIMARY invariant, checked on a SINGLE edge (independent per-edge checks + * could false-green by combining a strategy-less lucky edge with an unrelated + * lsp_ts-tagged edge): there exists a CALLS edge whose + * - source is callable-sourced (Function/Method) and its QN contains + * `caller_substr`; + * - target QN ends with `callee_suffix`; + * - properties_json carries `strategy_substr` (substring, so any concrete + * "lsp_ts_*" tag satisfies "lsp_ts"). + * When `dump` is non-zero every CALLS edge is printed to stderr so a RED run + * documents exactly what the graph contains instead. + */ +static int lsp_resolved_edge_exists(cbm_store_t *store, const char *project, + const char *caller_substr, + const char *callee_suffix, + const char *strategy_substr, int dump) { + cbm_edge_t *edges = NULL; + int n = 0; + if (cbm_store_find_edges_by_type(store, project, "CALLS", &edges, &n) != + CBM_STORE_OK) + return 0; + int found = 0; + size_t cl = strlen(callee_suffix); + for (int i = 0; i < n; i++) { + cbm_node_t src, tgt; + if (cbm_store_find_node_by_id(store, edges[i].source_id, &src) != CBM_STORE_OK) + continue; + if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK) + continue; + if (dump) + fprintf(stderr, " [ts-inherited] CALLS %s (%s) -> %s props=%s\n", + src.qualified_name ? src.qualified_name : "?", + src.label ? src.label : "?", + tgt.qualified_name ? tgt.qualified_name : "?", + edges[i].properties_json ? edges[i].properties_json : "{}"); + const char *slabel = src.label ? src.label : ""; + if (strcmp(slabel, "Function") != 0 && strcmp(slabel, "Method") != 0) + continue; + if (!src.qualified_name || !strstr(src.qualified_name, caller_substr)) + continue; + const char *tqn = tgt.qualified_name; + if (!tqn) + continue; + size_t tl = strlen(tqn); + if (tl < cl || strcmp(tqn + tl - cl, callee_suffix) != 0) + continue; + if (!edges[i].properties_json || + !strstr(edges[i].properties_json, strategy_substr)) + continue; + found = 1; + } + cbm_store_free_edges(edges, n); + return found; +} + +/* ── Tests ───────────────────────────────────────────────────────────────── */ + +/* + * Extraction-tier preconditions — expected GREEN on current code. These prove + * a red in the pipeline test below is the RESOLUTION gap, not a fixture or + * extraction error: both files parse without has_error, base.ts yields the + * Method def `greet`, and derived.ts yields the `greet` call site. + */ +TEST(repro_ts_inherited_extraction_preconditions) { + /* base.ts extracts cleanly and defines Method greet. */ + ASSERT_TRUE(inv_extract_clean(kTsInherited[0].content, CBM_LANG_TYPESCRIPT, + "base.ts")); + CBMFileResult *rb = + inv_rx(kTsInherited[0].content, CBM_LANG_TYPESCRIPT, "base.ts"); + ASSERT_NOT_NULL(rb); + int greet_methods = 0; + for (int i = 0; i < rb->defs.count; i++) { + CBMDefinition *d = &rb->defs.items[i]; + if (d->label && strcmp(d->label, "Method") == 0 && d->name && + strcmp(d->name, "greet") == 0) + greet_methods++; + } + cbm_free_result(rb); + ASSERT_EQ(greet_methods, 1); + + /* derived.ts extracts cleanly and contains the greet call site. */ + ASSERT_TRUE(inv_extract_clean(kTsInherited[1].content, CBM_LANG_TYPESCRIPT, + "derived.ts")); + CBMFileResult *rd = + inv_rx(kTsInherited[1].content, CBM_LANG_TYPESCRIPT, "derived.ts"); + ASSERT_NOT_NULL(rd); + int has_greet_call = inv_has_call(rd, "greet"); + cbm_free_result(rd); + ASSERT_TRUE(has_greet_call); + PASS(); +} + +/* + * THE RED REPRODUCTION — index the 2-file fixture through the full production + * pipeline and assert the CORRECT outcome: an LSP-resolved CALLS edge from + * callSite to Base.greet. Store-level preconditions (callee node present, + * INHERITS extracted) are checked first so the failure is attributable to the + * missing ts_lsp cross-file inheritance resolution and nothing else. + * Returns 0 on PASS (gap fixed), non-zero on FAIL (RED = the open gap). + */ +static int run_ts_inherited_pipeline(void) { + RProj lp; + cbm_store_t *store = rh_index_files( + &lp, kTsInherited, (int)(sizeof(kTsInherited) / sizeof(kTsInherited[0]))); + if (!store) { + printf(" %sFAIL%s %s:%d: index failed (setup, NOT the gap)\n", tf_red(), + tf_reset(), __FILE__, __LINE__); + rh_cleanup(&lp, store); + return 1; + } + + int rc = 0; + + /* Precondition: the callee def node exists in the graph. */ + if (!node_with_qn_suffix(store, lp.project, "Method", ".Base.greet")) { + printf(" %sFAIL%s %s:%d: precondition — no Method node with QN suffix " + "\".Base.greet\" (extraction problem, NOT the resolution gap)\n", + tf_red(), tf_reset(), __FILE__, __LINE__); + rc = 1; + } + + /* Precondition: `Derived extends Base` produced an INHERITS edge (the S6 + * probe diagnostic confirms extraction emits it; the gap is downstream). */ + int inherits = rh_count_edges(store, lp.project, "INHERITS"); + if (inherits < 1) { + printf(" %sFAIL%s %s:%d: precondition — INHERITS=%d (expected >=1; " + "extraction problem, NOT the resolution gap)\n", + tf_red(), tf_reset(), __FILE__, __LINE__, inherits); + rc = 1; + } + + /* PRIMARY: the inherited call is resolved BY THE TS LSP — a CALLS edge + * callSite -> *.Base.greet carrying an "lsp_ts" strategy. A short-name + * registry fallback edge (no lsp_ts tag) does NOT satisfy this; nor does + * post-#840 suppression (no edge at all). */ + if (!lsp_resolved_edge_exists(store, lp.project, "callSite", ".Base.greet", + "lsp_ts", 0)) { + /* Dump what the graph actually contains so the RED row documents it. */ + (void)lsp_resolved_edge_exists(store, lp.project, "callSite", + ".Base.greet", "lsp_ts", 1); + printf(" %sFAIL%s %s:%d: no lsp_ts-resolved CALLS edge callSite -> " + "*.Base.greet — TS cross-file INHERITED method call is " + "UNRESOLVED (ts_lsp inheritance gap, see #836/#840)\n", + tf_red(), tf_reset(), __FILE__, __LINE__); + rc = 1; + } + + rh_cleanup(&lp, store); + return rc; +} + +TEST(repro_ts_inherited_method_call_resolution) { + return run_ts_inherited_pipeline(); +} + +/* ── Suite ───────────────────────────────────────────────────────────────── */ + +SUITE(repro_ts_inherited_method) { + RUN_TEST(repro_ts_inherited_extraction_preconditions); + RUN_TEST(repro_ts_inherited_method_call_resolution); +}