pi-revit 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,313 +1,313 @@
1
- # -*- coding: utf-8 -*-
2
- """Reproducible benchmark of pi-revit search_api_docs against RevitAPI.xml ground truth.
3
-
4
- Usage: python scripts/benchmark-search-docs.py [results.json] [path-to-RevitAPI.xml]
5
- Requires: Revit running with the pi-revit bridge add-in loaded (a document need not
6
- be open) and Python 3.8+. Ground truth is Autodesk's own RevitAPI.xml, so every
7
- number is reproducible on any machine with the same Revit version.
8
-
9
- Categories:
10
- A exact-name recall/ranking B signature + spacing variants
11
- C complex syntax (arrays, nested generics, ref/out, ctors)
12
- D namespace ambiguity E honesty/adversarial controls
13
- F documentation fidelity G latency stats
14
- """
15
- import glob, json, os, random, re, sys, time, urllib.request, xml.etree.ElementTree as ET
16
- from collections import defaultdict
17
-
18
- random.seed(20260722) # reproducible sampling
19
-
20
- APPDATA = os.environ.get("APPDATA") or os.path.expanduser(r"~\AppData\Roaming")
21
- BRIDGE = json.load(open(os.path.join(APPDATA, "RevitBridge", "bridge.json")))
22
- BASE, TOKEN = BRIDGE["baseUrl"], BRIDGE["token"]
23
- if len(sys.argv) > 2:
24
- XML_PATH = sys.argv[2]
25
- else:
26
- revit_version = BRIDGE.get("revitVersion", "")
27
- candidates = glob.glob(rf"C:\Program Files\Autodesk\Revit {revit_version}*\RevitAPI.xml") \
28
- or glob.glob(r"C:\Program Files\Autodesk\Revit 20*\RevitAPI.xml")
29
- if not candidates:
30
- sys.exit("RevitAPI.xml not found; pass its path as the second argument.")
31
- XML_PATH = candidates[-1]
32
-
33
- LAT = []
34
- def search(query, max_results=10):
35
- body = json.dumps({"query": query, "max_results": max_results}).encode("utf-8")
36
- url = f"{BASE}/tools/search_api_docs/execute?token={TOKEN}&timeout_ms=60000"
37
- req = urllib.request.Request(url, data=body, headers={"content-type": "application/json"}, method="POST")
38
- t0 = time.perf_counter()
39
- with urllib.request.urlopen(req, timeout=90) as resp:
40
- payload = json.load(resp)
41
- LAT.append((time.perf_counter() - t0) * 1000)
42
- det = (payload.get("details") or {}).get("payload") or {}
43
- return det.get("matches") or [], det.get("totalMatches", 0)
44
-
45
- # ---------------------------------------------------------------- ground truth
46
- PRIM = {"System.String": "string", "System.Boolean": "bool", "System.Double": "double",
47
- "System.Int32": "int", "System.Int64": "long", "System.Object": "object",
48
- "System.Byte": "byte", "System.Single": "float", "System.UInt32": "uint"}
49
-
50
- def split_top(params): # split on commas outside {}
51
- out, depth, cur = [], 0, ""
52
- for ch in params:
53
- if ch == "{": depth += 1
54
- elif ch == "}": depth -= 1
55
- if ch == "," and depth == 0: out.append(cur); cur = ""
56
- else: cur += ch
57
- if cur: out.append(cur)
58
- return out
59
-
60
- def short_type(t):
61
- t = t.strip()
62
- byref = t.endswith("@")
63
- if byref: t = t[:-1]
64
- array = ""
65
- while t.endswith("[]"): array += "[]"; t = t[:-2]
66
- if t in PRIM: base = PRIM[t]
67
- elif "{" in t:
68
- outer, inner = t[:t.index("{")], t[t.index("{")+1:t.rindex("}")]
69
- outer = outer.split(".")[-1].split("`")[0]
70
- base = f"{outer}<{', '.join(short_type(x) for x in split_top(inner))}>"
71
- else:
72
- base = t.split(".")[-1]
73
- return base + array + ("&" if byref else "")
74
-
75
- class Member:
76
- __slots__ = ("kind","full","typename","member","params","summary","pnames","since")
77
- def __init__(s, kind, full, typename, member, params, summary, pnames, since):
78
- s.kind, s.full, s.typename, s.member = kind, full, typename, member
79
- s.params, s.summary, s.pnames, s.since = params, summary, pnames, since
80
- @property
81
- def short_sig_prefix(s):
82
- shorts = [short_type(p) for p in s.params]
83
- head = s.typename if s.member == "#ctor" else f"{s.typename}.{s.member}"
84
- return f"{head}(" + ", ".join(shorts)
85
-
86
- def text_of(el):
87
- if el is None: return None
88
- return re.sub(r"\s+", " ", "".join(el.itertext())).strip() or None
89
-
90
- print("parsing RevitAPI.xml ...", flush=True)
91
- root = ET.parse(XML_PATH).getroot()
92
- methods, props, types = [], [], []
93
- for m in root.iter("member"):
94
- name = m.get("name", "")
95
- summary = text_of(m.find("summary"))
96
- since = text_of(m.find("since"))
97
- pnames = [p.get("name") for p in m.findall("param")]
98
- if name.startswith("M:"):
99
- body = name[2:]
100
- params = ""
101
- if "(" in body:
102
- body, params = body[:body.index("(")], body[body.index("(")+1:body.rindex(")")]
103
- parts = body.split(".")
104
- if len(parts) < 2: continue
105
- member, typename = parts[-1], parts[-2]
106
- if "`" in typename or "`" in member: continue
107
- full = ".".join(parts)
108
- methods.append(Member("M", full, typename, member, split_top(params) if params else [], summary, pnames, since))
109
- elif name.startswith("P:"):
110
- parts = name[2:].split(".")
111
- if len(parts) < 2 or "`" in parts[-2]: continue
112
- props.append(Member("P", name[2:], parts[-2], parts[-1], [], summary, pnames, since))
113
- elif name.startswith("T:"):
114
- parts = name[2:].split(".")
115
- if "`" in parts[-1]: continue
116
- types.append(Member("T", name[2:], parts[-1], "", [], summary, pnames, since))
117
-
118
- print(f"golden pool: {len(methods)} methods, {len(props)} properties, {len(types)} types", flush=True)
119
-
120
- by_ns = defaultdict(list)
121
- for mm in methods: by_ns[".".join(mm.full.split(".")[:-2])].append(mm)
122
- real_methods = {(m.typename, m.member) for m in methods}
123
- overload_count = defaultdict(int)
124
- for m in methods: overload_count[(m.typename, m.member)] += 1
125
-
126
- def stratified(pool, n):
127
- groups = defaultdict(list)
128
- for m in pool: groups[".".join(m.full.split(".")[:4])].append(m)
129
- out, keys = [], sorted(groups)
130
- while len(out) < n and keys:
131
- for k in list(keys):
132
- if groups[k]: out.append(groups[k].pop(random.randrange(len(groups[k]))))
133
- else: keys.remove(k)
134
- if len(out) >= n: break
135
- return out
136
-
137
- R = {} # results
138
-
139
- # ---------------------------------------------------------- A: exact-name recall
140
- A = {"n":0,"top1":0,"top3":0,"recall":0}
141
- sampleA = stratified(methods, 90) + stratified(props, 40) + stratified(types, 30)
142
- for m in sampleA:
143
- q = f"{m.typename}.{m.member}" if m.kind in "MP" else m.typename
144
- matches, _ = search(q)
145
- target = m.full if m.kind in "MP" else m.full
146
- names = [x.get("name","") for x in matches]
147
- A["n"] += 1
148
- A["recall"] += any(n == target for n in names)
149
- A["top1"] += bool(names and names[0] == target)
150
- A["top3"] += any(n == target for n in names[:3])
151
- print("A done", A, flush=True)
152
- R["A exact-name"] = A
153
-
154
- # ------------------------------------------------- B: signatures + spacing variants
155
- B = {"n":0,"top1_sig":0,"variants_agree":0,"variant_sets":0}
156
- sigpool = [m for m in methods if 2 <= len(m.params) <= 5]
157
- sampleB = stratified(sigpool, 60)
158
- for m in sampleB:
159
- try: prefix = m.short_sig_prefix
160
- except Exception: continue
161
- canonical = prefix # "Type.Member(P1, P2..."
162
- matches, _ = search(canonical)
163
- B["n"] += 1
164
- sigs = [x.get("signature","") for x in matches]
165
- want = f"new {m.typename}(" if m.member == "#ctor" else f"{m.typename}.{m.member}("
166
- ok = bool(sigs and sigs[0].startswith(want))
167
- B["top1_sig"] += ok
168
- variants = [canonical.replace(", ", ","),
169
- canonical.replace(", ", " , "),
170
- canonical.replace(", ", ", "),
171
- canonical.replace("(", "( ", 1)]
172
- counts = {len(matches)}
173
- for v in variants:
174
- vm, _ = search(v)
175
- counts.add(len(vm))
176
- B["variant_sets"] += 1
177
- B["variants_agree"] += (len(counts) == 1)
178
- print("B done", B, flush=True)
179
- R["B signature+spacing"] = B
180
-
181
- # --------------------------------------------------------- C: complex syntax shapes
182
- C = {"cases": []}
183
- def probe(label, q, expect_nonzero=True):
184
- try: matches, total = search(q)
185
- except Exception as e:
186
- C["cases"].append((label, q, f"ERROR {e}")); return
187
- ok = (total > 0) if expect_nonzero else (total == 0)
188
- top = matches[0]["signature"] if matches else "-"
189
- C["cases"].append((label, q, f"{'OK' if ok else 'MISS'} total={total} top={top[:70]}"))
190
-
191
- arr = next((m for m in methods if any(p.endswith("[]") for p in m.params)), None)
192
- if arr: probe("array param", arr.short_sig_prefix)
193
- nest = next((m for m in methods if any("{" in p and "{" in p[p.index("{")+1:] for p in m.params)), None)
194
- if nest: probe("nested generic", nest.short_sig_prefix)
195
- byref = next((m for m in methods if any(p.endswith("@") for p in m.params)), None)
196
- if byref: probe("ref/out param", byref.short_sig_prefix)
197
- ctor = next((m for m in methods if m.member == "#ctor" and m.typename == "FilteredElementCollector"), None)
198
- probe("constructor", "FilteredElementCollector(Document")
199
- probe("property", "PDFExportOptions.FileName")
200
- probe("fully qualified type", "Autodesk.Revit.DB.Document")
201
- probe("case-insensitive", "wall.create(document, curve")
202
- probe("Double vs double", "UnitUtils.ConvertToInternalUnits(Double, ForgeTypeId")
203
- probe("trailing garbage brackets", "Wall.Create(Document, Curve]]", True) # observational
204
- probe("double periods", "Wall..Create", True) # observational
205
- # 0.2.11: CLR alias + namespace reduction in signature queries, Creation-factory rewrite
206
- probe("CLR alias Boolean", "Wall.Create(Document, Curve, ElementId, Boolean")
207
- probe("qualified param type", "Wall.Create(Autodesk.Revit.DB.Document, Curve")
208
- probe("System-qualified alias", "ParameterFilterElement.Create(Document, System.String")
209
- probe("factory rewrite", "Document.Create.NewRoom(Level, UV")
210
- probe("factory inherited member", "Document.Create.NewFamilyInstance")
211
- probe("factory honesty control", "Document.Create.Banana", expect_nonzero=False)
212
- print("C done", flush=True)
213
- R["C complex-syntax"] = C
214
-
215
- # ------------------------------------------------------------ D: namespace ambiguity
216
- D = {"cases": []}
217
- def ns_case(q, want_substr):
218
- matches, total = search(q)
219
- top = matches[0]["name"] if matches else "-"
220
- D["cases"].append((q, want_substr, top, "OK" if want_substr in top else "MISS"))
221
- for q, want in [
222
- ("Autodesk.Revit.DB.Architecture.Room", ".Architecture.Room"),
223
- ("Autodesk.Revit.DB.Mechanical.Space", ".Mechanical.Space"),
224
- ("Architecture.Room", ".Architecture.Room"),
225
- ("Mechanical.Duct", ".Mechanical.Duct"),
226
- ("Plumbing.Pipe", ".Plumbing.Pipe"),
227
- ("Electrical.Wire", ".Electrical.Wire"),
228
- ("Structure.Rebar", ".Structure.Rebar"),
229
- ("Room", "Room"), ("Space", "Space"), ("Area", "Area"),
230
- ("Connector", "Connector"),
231
- ]: ns_case(q, want)
232
- print("D done", flush=True)
233
- R["D namespace"] = D
234
-
235
- # ------------------------------------------------------- E: honesty / adversarial
236
- E = {"n":0,"exact_fp":0,"zero":0,"kinds":defaultdict(lambda:[0,0,0])}
237
- adv = []
238
- pool = stratified([m for m in methods if 1 <= len(m.params) <= 4 and m.member != "#ctor"], 120)
239
- for m in pool[:30]: # misspelled member (drop 2nd letter)
240
- if len(m.member) < 5: continue
241
- adv.append(("misspelled-member", f"{m.typename}.{m.member[0]+m.member[2:]}", m))
242
- for m in pool[30:60]: # nonsense parameter
243
- adv.append(("nonsense-param", f"{m.typename}.{m.member}(Banana", m))
244
- for m in pool[60:90]: # wrong declaring class
245
- other = random.choice(types).typename
246
- if (other, m.member) in real_methods: continue
247
- adv.append(("wrong-class", f"{other}.{m.member}", (other, m.member)))
248
- for m in pool[90:120]: # swapped params (only if that order is not a real overload)
249
- if len(m.params) < 2 or m.params[0] == m.params[1]: continue
250
- sw = [m.params[1], m.params[0]] + m.params[2:]
251
- exists = any(o.params[:len(sw)] == sw for o in methods if (o.typename,o.member)==(m.typename,m.member))
252
- if exists: continue
253
- shorts = [short_type(p) for p in sw]
254
- adv.append(("swapped-params", f"{m.typename}.{m.member}(" + ", ".join(shorts), m))
255
- for kind, q, _meta in adv:
256
- matches, total = search(q)
257
- E["n"] += 1
258
- zero = (total == 0)
259
- # exact false positive: a returned entry CLAIMS the impossible thing exists
260
- fp = False
261
- if kind == "wrong-class":
262
- t, mem = _meta
263
- fp = any(x.get("name","").endswith(f".{t}.{mem}") or f".{t}." in x.get("name","") and x.get("name","").endswith(f".{mem}") for x in matches)
264
- elif kind == "swapped-params":
265
- fp = any(x.get("signature","").lower() == (q.lower()+")") for x in matches)
266
- elif kind == "nonsense-param":
267
- fp = any("banana" in x.get("signature","").lower() for x in matches)
268
- elif kind == "misspelled-member":
269
- bad = q.split(".")[-1].lower()
270
- fp = any(x.get("name","").lower().endswith("."+bad) for x in matches)
271
- E["exact_fp"] += fp
272
- E["zero"] += zero
273
- k = E["kinds"][kind]; k[0] += 1; k[1] += fp; k[2] += zero
274
- print("E done", {k: list(v) for k, v in E["kinds"].items()}, flush=True)
275
- E["kinds"] = {k: list(v) for k, v in E["kinds"].items()}
276
- R["E honesty"] = E
277
-
278
- # ------------------------------------------------------------- F: doc fidelity
279
- F = {"n":0,"param_names_ok":0,"summary_ok":0,"since_ok":0,"since_present":0,"overload_docs_ok":0,"overload_checked":0}
280
- fsample = stratified([m for m in methods if m.pnames and m.summary and m.member != "#ctor"], 50)
281
- for m in fsample:
282
- try: q = m.short_sig_prefix
283
- except Exception: continue
284
- matches, _ = search(q)
285
- if not matches: continue
286
- top = matches[0]
287
- F["n"] += 1
288
- got_pnames = [p.get("name") for p in (top.get("parameters") or [])]
289
- F["param_names_ok"] += (got_pnames == m.pnames)
290
- gs, ws = (top.get("summary") or ""), m.summary
291
- F["summary_ok"] += (re.sub(r"\W","",gs)[:80] == re.sub(r"\W","",ws)[:80])
292
- if m.since:
293
- F["since_present"] += 1
294
- F["since_ok"] += (top.get("since") == m.since)
295
- # overload-doc attribution: right param COUNT on the top signature
296
- if overload_count[(m.typename, m.member)] > 1:
297
- F["overload_checked"] += 1
298
- sig = top.get("signature","")
299
- inner = sig[sig.index("(")+1:sig.rindex(")")] if "(" in sig else ""
300
- F["overload_docs_ok"] += (len(split_top(inner)) == len(m.params) if inner else len(m.params)==0)
301
- print("F done", F, flush=True)
302
- R["F fidelity"] = F
303
-
304
- # ------------------------------------------------------------------ G: latency
305
- LAT.sort()
306
- def pct(p): return LAT[min(len(LAT)-1, int(len(LAT)*p))]
307
- R["G latency"] = {"queries": len(LAT), "cold_first_ms": round(LAT[-1] if not LAT else None or 0),
308
- "p50_ms": round(pct(0.50)), "p95_ms": round(pct(0.95)), "p99_ms": round(pct(0.99)),
309
- "max_ms": round(LAT[-1])}
310
-
311
- json.dump(R, open(sys.argv[1] if len(sys.argv)>1 else "benchmark-results.json","w"), indent=1, default=str)
312
- print("ALL DONE", flush=True)
313
- print(json.dumps(R, indent=1, default=str)[:3000])
1
+ # -*- coding: utf-8 -*-
2
+ """Reproducible benchmark of pi-revit search_api_docs against RevitAPI.xml ground truth.
3
+
4
+ Usage: python scripts/benchmark-search-docs.py [results.json] [path-to-RevitAPI.xml]
5
+ Requires: Revit running with the pi-revit bridge add-in loaded (a document need not
6
+ be open) and Python 3.8+. Ground truth is Autodesk's own RevitAPI.xml, so every
7
+ number is reproducible on any machine with the same Revit version.
8
+
9
+ Categories:
10
+ A exact-name recall/ranking B signature + spacing variants
11
+ C complex syntax (arrays, nested generics, ref/out, ctors)
12
+ D namespace ambiguity E honesty/adversarial controls
13
+ F documentation fidelity G latency stats
14
+ """
15
+ import glob, json, os, random, re, sys, time, urllib.request, xml.etree.ElementTree as ET
16
+ from collections import defaultdict
17
+
18
+ random.seed(20260722) # reproducible sampling
19
+
20
+ APPDATA = os.environ.get("APPDATA") or os.path.expanduser(r"~\AppData\Roaming")
21
+ BRIDGE = json.load(open(os.path.join(APPDATA, "RevitBridge", "bridge.json")))
22
+ BASE, TOKEN = BRIDGE["baseUrl"], BRIDGE["token"]
23
+ if len(sys.argv) > 2:
24
+ XML_PATH = sys.argv[2]
25
+ else:
26
+ revit_version = BRIDGE.get("revitVersion", "")
27
+ candidates = glob.glob(rf"C:\Program Files\Autodesk\Revit {revit_version}*\RevitAPI.xml") \
28
+ or glob.glob(r"C:\Program Files\Autodesk\Revit 20*\RevitAPI.xml")
29
+ if not candidates:
30
+ sys.exit("RevitAPI.xml not found; pass its path as the second argument.")
31
+ XML_PATH = candidates[-1]
32
+
33
+ LAT = []
34
+ def search(query, max_results=10):
35
+ body = json.dumps({"query": query, "max_results": max_results}).encode("utf-8")
36
+ url = f"{BASE}/tools/search_api_docs/execute?token={TOKEN}&timeout_ms=60000"
37
+ req = urllib.request.Request(url, data=body, headers={"content-type": "application/json"}, method="POST")
38
+ t0 = time.perf_counter()
39
+ with urllib.request.urlopen(req, timeout=90) as resp:
40
+ payload = json.load(resp)
41
+ LAT.append((time.perf_counter() - t0) * 1000)
42
+ det = (payload.get("details") or {}).get("payload") or {}
43
+ return det.get("matches") or [], det.get("totalMatches", 0)
44
+
45
+ # ---------------------------------------------------------------- ground truth
46
+ PRIM = {"System.String": "string", "System.Boolean": "bool", "System.Double": "double",
47
+ "System.Int32": "int", "System.Int64": "long", "System.Object": "object",
48
+ "System.Byte": "byte", "System.Single": "float", "System.UInt32": "uint"}
49
+
50
+ def split_top(params): # split on commas outside {}
51
+ out, depth, cur = [], 0, ""
52
+ for ch in params:
53
+ if ch == "{": depth += 1
54
+ elif ch == "}": depth -= 1
55
+ if ch == "," and depth == 0: out.append(cur); cur = ""
56
+ else: cur += ch
57
+ if cur: out.append(cur)
58
+ return out
59
+
60
+ def short_type(t):
61
+ t = t.strip()
62
+ byref = t.endswith("@")
63
+ if byref: t = t[:-1]
64
+ array = ""
65
+ while t.endswith("[]"): array += "[]"; t = t[:-2]
66
+ if t in PRIM: base = PRIM[t]
67
+ elif "{" in t:
68
+ outer, inner = t[:t.index("{")], t[t.index("{")+1:t.rindex("}")]
69
+ outer = outer.split(".")[-1].split("`")[0]
70
+ base = f"{outer}<{', '.join(short_type(x) for x in split_top(inner))}>"
71
+ else:
72
+ base = t.split(".")[-1]
73
+ return base + array + ("&" if byref else "")
74
+
75
+ class Member:
76
+ __slots__ = ("kind","full","typename","member","params","summary","pnames","since")
77
+ def __init__(s, kind, full, typename, member, params, summary, pnames, since):
78
+ s.kind, s.full, s.typename, s.member = kind, full, typename, member
79
+ s.params, s.summary, s.pnames, s.since = params, summary, pnames, since
80
+ @property
81
+ def short_sig_prefix(s):
82
+ shorts = [short_type(p) for p in s.params]
83
+ head = s.typename if s.member == "#ctor" else f"{s.typename}.{s.member}"
84
+ return f"{head}(" + ", ".join(shorts)
85
+
86
+ def text_of(el):
87
+ if el is None: return None
88
+ return re.sub(r"\s+", " ", "".join(el.itertext())).strip() or None
89
+
90
+ print("parsing RevitAPI.xml ...", flush=True)
91
+ root = ET.parse(XML_PATH).getroot()
92
+ methods, props, types = [], [], []
93
+ for m in root.iter("member"):
94
+ name = m.get("name", "")
95
+ summary = text_of(m.find("summary"))
96
+ since = text_of(m.find("since"))
97
+ pnames = [p.get("name") for p in m.findall("param")]
98
+ if name.startswith("M:"):
99
+ body = name[2:]
100
+ params = ""
101
+ if "(" in body:
102
+ body, params = body[:body.index("(")], body[body.index("(")+1:body.rindex(")")]
103
+ parts = body.split(".")
104
+ if len(parts) < 2: continue
105
+ member, typename = parts[-1], parts[-2]
106
+ if "`" in typename or "`" in member: continue
107
+ full = ".".join(parts)
108
+ methods.append(Member("M", full, typename, member, split_top(params) if params else [], summary, pnames, since))
109
+ elif name.startswith("P:"):
110
+ parts = name[2:].split(".")
111
+ if len(parts) < 2 or "`" in parts[-2]: continue
112
+ props.append(Member("P", name[2:], parts[-2], parts[-1], [], summary, pnames, since))
113
+ elif name.startswith("T:"):
114
+ parts = name[2:].split(".")
115
+ if "`" in parts[-1]: continue
116
+ types.append(Member("T", name[2:], parts[-1], "", [], summary, pnames, since))
117
+
118
+ print(f"golden pool: {len(methods)} methods, {len(props)} properties, {len(types)} types", flush=True)
119
+
120
+ by_ns = defaultdict(list)
121
+ for mm in methods: by_ns[".".join(mm.full.split(".")[:-2])].append(mm)
122
+ real_methods = {(m.typename, m.member) for m in methods}
123
+ overload_count = defaultdict(int)
124
+ for m in methods: overload_count[(m.typename, m.member)] += 1
125
+
126
+ def stratified(pool, n):
127
+ groups = defaultdict(list)
128
+ for m in pool: groups[".".join(m.full.split(".")[:4])].append(m)
129
+ out, keys = [], sorted(groups)
130
+ while len(out) < n and keys:
131
+ for k in list(keys):
132
+ if groups[k]: out.append(groups[k].pop(random.randrange(len(groups[k]))))
133
+ else: keys.remove(k)
134
+ if len(out) >= n: break
135
+ return out
136
+
137
+ R = {} # results
138
+
139
+ # ---------------------------------------------------------- A: exact-name recall
140
+ A = {"n":0,"top1":0,"top3":0,"recall":0}
141
+ sampleA = stratified(methods, 90) + stratified(props, 40) + stratified(types, 30)
142
+ for m in sampleA:
143
+ q = f"{m.typename}.{m.member}" if m.kind in "MP" else m.typename
144
+ matches, _ = search(q)
145
+ target = m.full if m.kind in "MP" else m.full
146
+ names = [x.get("name","") for x in matches]
147
+ A["n"] += 1
148
+ A["recall"] += any(n == target for n in names)
149
+ A["top1"] += bool(names and names[0] == target)
150
+ A["top3"] += any(n == target for n in names[:3])
151
+ print("A done", A, flush=True)
152
+ R["A exact-name"] = A
153
+
154
+ # ------------------------------------------------- B: signatures + spacing variants
155
+ B = {"n":0,"top1_sig":0,"variants_agree":0,"variant_sets":0}
156
+ sigpool = [m for m in methods if 2 <= len(m.params) <= 5]
157
+ sampleB = stratified(sigpool, 60)
158
+ for m in sampleB:
159
+ try: prefix = m.short_sig_prefix
160
+ except Exception: continue
161
+ canonical = prefix # "Type.Member(P1, P2..."
162
+ matches, _ = search(canonical)
163
+ B["n"] += 1
164
+ sigs = [x.get("signature","") for x in matches]
165
+ want = f"new {m.typename}(" if m.member == "#ctor" else f"{m.typename}.{m.member}("
166
+ ok = bool(sigs and sigs[0].startswith(want))
167
+ B["top1_sig"] += ok
168
+ variants = [canonical.replace(", ", ","),
169
+ canonical.replace(", ", " , "),
170
+ canonical.replace(", ", ", "),
171
+ canonical.replace("(", "( ", 1)]
172
+ counts = {len(matches)}
173
+ for v in variants:
174
+ vm, _ = search(v)
175
+ counts.add(len(vm))
176
+ B["variant_sets"] += 1
177
+ B["variants_agree"] += (len(counts) == 1)
178
+ print("B done", B, flush=True)
179
+ R["B signature+spacing"] = B
180
+
181
+ # --------------------------------------------------------- C: complex syntax shapes
182
+ C = {"cases": []}
183
+ def probe(label, q, expect_nonzero=True):
184
+ try: matches, total = search(q)
185
+ except Exception as e:
186
+ C["cases"].append((label, q, f"ERROR {e}")); return
187
+ ok = (total > 0) if expect_nonzero else (total == 0)
188
+ top = matches[0]["signature"] if matches else "-"
189
+ C["cases"].append((label, q, f"{'OK' if ok else 'MISS'} total={total} top={top[:70]}"))
190
+
191
+ arr = next((m for m in methods if any(p.endswith("[]") for p in m.params)), None)
192
+ if arr: probe("array param", arr.short_sig_prefix)
193
+ nest = next((m for m in methods if any("{" in p and "{" in p[p.index("{")+1:] for p in m.params)), None)
194
+ if nest: probe("nested generic", nest.short_sig_prefix)
195
+ byref = next((m for m in methods if any(p.endswith("@") for p in m.params)), None)
196
+ if byref: probe("ref/out param", byref.short_sig_prefix)
197
+ ctor = next((m for m in methods if m.member == "#ctor" and m.typename == "FilteredElementCollector"), None)
198
+ probe("constructor", "FilteredElementCollector(Document")
199
+ probe("property", "PDFExportOptions.FileName")
200
+ probe("fully qualified type", "Autodesk.Revit.DB.Document")
201
+ probe("case-insensitive", "wall.create(document, curve")
202
+ probe("Double vs double", "UnitUtils.ConvertToInternalUnits(Double, ForgeTypeId")
203
+ probe("trailing garbage brackets", "Wall.Create(Document, Curve]]", True) # observational
204
+ probe("double periods", "Wall..Create", True) # observational
205
+ # 0.2.11: CLR alias + namespace reduction in signature queries, Creation-factory rewrite
206
+ probe("CLR alias Boolean", "Wall.Create(Document, Curve, ElementId, Boolean")
207
+ probe("qualified param type", "Wall.Create(Autodesk.Revit.DB.Document, Curve")
208
+ probe("System-qualified alias", "ParameterFilterElement.Create(Document, System.String")
209
+ probe("factory rewrite", "Document.Create.NewRoom(Level, UV")
210
+ probe("factory inherited member", "Document.Create.NewFamilyInstance")
211
+ probe("factory honesty control", "Document.Create.Banana", expect_nonzero=False)
212
+ print("C done", flush=True)
213
+ R["C complex-syntax"] = C
214
+
215
+ # ------------------------------------------------------------ D: namespace ambiguity
216
+ D = {"cases": []}
217
+ def ns_case(q, want_substr):
218
+ matches, total = search(q)
219
+ top = matches[0]["name"] if matches else "-"
220
+ D["cases"].append((q, want_substr, top, "OK" if want_substr in top else "MISS"))
221
+ for q, want in [
222
+ ("Autodesk.Revit.DB.Architecture.Room", ".Architecture.Room"),
223
+ ("Autodesk.Revit.DB.Mechanical.Space", ".Mechanical.Space"),
224
+ ("Architecture.Room", ".Architecture.Room"),
225
+ ("Mechanical.Duct", ".Mechanical.Duct"),
226
+ ("Plumbing.Pipe", ".Plumbing.Pipe"),
227
+ ("Electrical.Wire", ".Electrical.Wire"),
228
+ ("Structure.Rebar", ".Structure.Rebar"),
229
+ ("Room", "Room"), ("Space", "Space"), ("Area", "Area"),
230
+ ("Connector", "Connector"),
231
+ ]: ns_case(q, want)
232
+ print("D done", flush=True)
233
+ R["D namespace"] = D
234
+
235
+ # ------------------------------------------------------- E: honesty / adversarial
236
+ E = {"n":0,"exact_fp":0,"zero":0,"kinds":defaultdict(lambda:[0,0,0])}
237
+ adv = []
238
+ pool = stratified([m for m in methods if 1 <= len(m.params) <= 4 and m.member != "#ctor"], 120)
239
+ for m in pool[:30]: # misspelled member (drop 2nd letter)
240
+ if len(m.member) < 5: continue
241
+ adv.append(("misspelled-member", f"{m.typename}.{m.member[0]+m.member[2:]}", m))
242
+ for m in pool[30:60]: # nonsense parameter
243
+ adv.append(("nonsense-param", f"{m.typename}.{m.member}(Banana", m))
244
+ for m in pool[60:90]: # wrong declaring class
245
+ other = random.choice(types).typename
246
+ if (other, m.member) in real_methods: continue
247
+ adv.append(("wrong-class", f"{other}.{m.member}", (other, m.member)))
248
+ for m in pool[90:120]: # swapped params (only if that order is not a real overload)
249
+ if len(m.params) < 2 or m.params[0] == m.params[1]: continue
250
+ sw = [m.params[1], m.params[0]] + m.params[2:]
251
+ exists = any(o.params[:len(sw)] == sw for o in methods if (o.typename,o.member)==(m.typename,m.member))
252
+ if exists: continue
253
+ shorts = [short_type(p) for p in sw]
254
+ adv.append(("swapped-params", f"{m.typename}.{m.member}(" + ", ".join(shorts), m))
255
+ for kind, q, _meta in adv:
256
+ matches, total = search(q)
257
+ E["n"] += 1
258
+ zero = (total == 0)
259
+ # exact false positive: a returned entry CLAIMS the impossible thing exists
260
+ fp = False
261
+ if kind == "wrong-class":
262
+ t, mem = _meta
263
+ fp = any(x.get("name","").endswith(f".{t}.{mem}") or f".{t}." in x.get("name","") and x.get("name","").endswith(f".{mem}") for x in matches)
264
+ elif kind == "swapped-params":
265
+ fp = any(x.get("signature","").lower() == (q.lower()+")") for x in matches)
266
+ elif kind == "nonsense-param":
267
+ fp = any("banana" in x.get("signature","").lower() for x in matches)
268
+ elif kind == "misspelled-member":
269
+ bad = q.split(".")[-1].lower()
270
+ fp = any(x.get("name","").lower().endswith("."+bad) for x in matches)
271
+ E["exact_fp"] += fp
272
+ E["zero"] += zero
273
+ k = E["kinds"][kind]; k[0] += 1; k[1] += fp; k[2] += zero
274
+ print("E done", {k: list(v) for k, v in E["kinds"].items()}, flush=True)
275
+ E["kinds"] = {k: list(v) for k, v in E["kinds"].items()}
276
+ R["E honesty"] = E
277
+
278
+ # ------------------------------------------------------------- F: doc fidelity
279
+ F = {"n":0,"param_names_ok":0,"summary_ok":0,"since_ok":0,"since_present":0,"overload_docs_ok":0,"overload_checked":0}
280
+ fsample = stratified([m for m in methods if m.pnames and m.summary and m.member != "#ctor"], 50)
281
+ for m in fsample:
282
+ try: q = m.short_sig_prefix
283
+ except Exception: continue
284
+ matches, _ = search(q)
285
+ if not matches: continue
286
+ top = matches[0]
287
+ F["n"] += 1
288
+ got_pnames = [p.get("name") for p in (top.get("parameters") or [])]
289
+ F["param_names_ok"] += (got_pnames == m.pnames)
290
+ gs, ws = (top.get("summary") or ""), m.summary
291
+ F["summary_ok"] += (re.sub(r"\W","",gs)[:80] == re.sub(r"\W","",ws)[:80])
292
+ if m.since:
293
+ F["since_present"] += 1
294
+ F["since_ok"] += (top.get("since") == m.since)
295
+ # overload-doc attribution: right param COUNT on the top signature
296
+ if overload_count[(m.typename, m.member)] > 1:
297
+ F["overload_checked"] += 1
298
+ sig = top.get("signature","")
299
+ inner = sig[sig.index("(")+1:sig.rindex(")")] if "(" in sig else ""
300
+ F["overload_docs_ok"] += (len(split_top(inner)) == len(m.params) if inner else len(m.params)==0)
301
+ print("F done", F, flush=True)
302
+ R["F fidelity"] = F
303
+
304
+ # ------------------------------------------------------------------ G: latency
305
+ LAT.sort()
306
+ def pct(p): return LAT[min(len(LAT)-1, int(len(LAT)*p))]
307
+ R["G latency"] = {"queries": len(LAT), "cold_first_ms": round(LAT[-1] if not LAT else None or 0),
308
+ "p50_ms": round(pct(0.50)), "p95_ms": round(pct(0.95)), "p99_ms": round(pct(0.99)),
309
+ "max_ms": round(LAT[-1])}
310
+
311
+ json.dump(R, open(sys.argv[1] if len(sys.argv)>1 else "benchmark-results.json","w"), indent=1, default=str)
312
+ print("ALL DONE", flush=True)
313
+ print(json.dumps(R, indent=1, default=str)[:3000])