pi-revit 0.2.12 → 0.2.14
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.
- package/CHANGELOG.md +294 -230
- package/README.md +3 -1
- package/bin/pi-revit.js +1 -1
- package/package.json +56 -56
- package/scripts/benchmark-search-docs.py +317 -313
- package/scripts/deploy.ps1 +12 -0
- package/scripts/setup-workspace.ps1 +80 -80
- package/scripts/uninstall.ps1 +146 -146
- package/skills/pi-revit/SKILL.md +53 -53
- package/src/Revit/BridgeServer.cs +67 -0
- package/src/Revit/Tools/CaptureView.cs +60 -1
- package/src/Revit/Tools/ExecuteCsharp.cs +17 -2
- package/src/Revit/Tools/ExportDocuments.cs +36 -5
- package/src/Revit/Tools/FailureGuard.cs +62 -62
- package/src/Revit/Tools/GetElements.cs +57 -19
- package/src/Revit/Tools/OpenView.cs +103 -103
- package/src/Revit/Tools/SearchApiDocs.cs +14 -0
- package/src/Revit/Tools/ToolSupport.cs +301 -301
- package/workspace/pi-revit-global.cmd +12 -12
|
@@ -1,313 +1,317 @@
|
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
("
|
|
227
|
-
("
|
|
228
|
-
("
|
|
229
|
-
("
|
|
230
|
-
("
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
for m in
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
adv.append(("
|
|
248
|
-
for m in pool[90
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
fp = any("
|
|
268
|
-
elif kind == "
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
E["
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
F["
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if
|
|
297
|
-
F["
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
# 0.2.14: C# accessor spelling (get_X/set_X) resolves to the documented property/indexer
|
|
213
|
+
probe("accessor indexer", "Element.get_Parameter(BuiltInParameter")
|
|
214
|
+
probe("accessor property", "Element.get_BoundingBox(View")
|
|
215
|
+
probe("literal get_ member", "LocationCurve.get_ElementsAtJoin")
|
|
216
|
+
print("C done", flush=True)
|
|
217
|
+
R["C complex-syntax"] = C
|
|
218
|
+
|
|
219
|
+
# ------------------------------------------------------------ D: namespace ambiguity
|
|
220
|
+
D = {"cases": []}
|
|
221
|
+
def ns_case(q, want_substr):
|
|
222
|
+
matches, total = search(q)
|
|
223
|
+
top = matches[0]["name"] if matches else "-"
|
|
224
|
+
D["cases"].append((q, want_substr, top, "OK" if want_substr in top else "MISS"))
|
|
225
|
+
for q, want in [
|
|
226
|
+
("Autodesk.Revit.DB.Architecture.Room", ".Architecture.Room"),
|
|
227
|
+
("Autodesk.Revit.DB.Mechanical.Space", ".Mechanical.Space"),
|
|
228
|
+
("Architecture.Room", ".Architecture.Room"),
|
|
229
|
+
("Mechanical.Duct", ".Mechanical.Duct"),
|
|
230
|
+
("Plumbing.Pipe", ".Plumbing.Pipe"),
|
|
231
|
+
("Electrical.Wire", ".Electrical.Wire"),
|
|
232
|
+
("Structure.Rebar", ".Structure.Rebar"),
|
|
233
|
+
("Room", "Room"), ("Space", "Space"), ("Area", "Area"),
|
|
234
|
+
("Connector", "Connector"),
|
|
235
|
+
]: ns_case(q, want)
|
|
236
|
+
print("D done", flush=True)
|
|
237
|
+
R["D namespace"] = D
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------- E: honesty / adversarial
|
|
240
|
+
E = {"n":0,"exact_fp":0,"zero":0,"kinds":defaultdict(lambda:[0,0,0])}
|
|
241
|
+
adv = []
|
|
242
|
+
pool = stratified([m for m in methods if 1 <= len(m.params) <= 4 and m.member != "#ctor"], 120)
|
|
243
|
+
for m in pool[:30]: # misspelled member (drop 2nd letter)
|
|
244
|
+
if len(m.member) < 5: continue
|
|
245
|
+
adv.append(("misspelled-member", f"{m.typename}.{m.member[0]+m.member[2:]}", m))
|
|
246
|
+
for m in pool[30:60]: # nonsense parameter
|
|
247
|
+
adv.append(("nonsense-param", f"{m.typename}.{m.member}(Banana", m))
|
|
248
|
+
for m in pool[60:90]: # wrong declaring class
|
|
249
|
+
other = random.choice(types).typename
|
|
250
|
+
if (other, m.member) in real_methods: continue
|
|
251
|
+
adv.append(("wrong-class", f"{other}.{m.member}", (other, m.member)))
|
|
252
|
+
for m in pool[90:120]: # swapped params (only if that order is not a real overload)
|
|
253
|
+
if len(m.params) < 2 or m.params[0] == m.params[1]: continue
|
|
254
|
+
sw = [m.params[1], m.params[0]] + m.params[2:]
|
|
255
|
+
exists = any(o.params[:len(sw)] == sw for o in methods if (o.typename,o.member)==(m.typename,m.member))
|
|
256
|
+
if exists: continue
|
|
257
|
+
shorts = [short_type(p) for p in sw]
|
|
258
|
+
adv.append(("swapped-params", f"{m.typename}.{m.member}(" + ", ".join(shorts), m))
|
|
259
|
+
for kind, q, _meta in adv:
|
|
260
|
+
matches, total = search(q)
|
|
261
|
+
E["n"] += 1
|
|
262
|
+
zero = (total == 0)
|
|
263
|
+
# exact false positive: a returned entry CLAIMS the impossible thing exists
|
|
264
|
+
fp = False
|
|
265
|
+
if kind == "wrong-class":
|
|
266
|
+
t, mem = _meta
|
|
267
|
+
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)
|
|
268
|
+
elif kind == "swapped-params":
|
|
269
|
+
fp = any(x.get("signature","").lower() == (q.lower()+")") for x in matches)
|
|
270
|
+
elif kind == "nonsense-param":
|
|
271
|
+
fp = any("banana" in x.get("signature","").lower() for x in matches)
|
|
272
|
+
elif kind == "misspelled-member":
|
|
273
|
+
bad = q.split(".")[-1].lower()
|
|
274
|
+
fp = any(x.get("name","").lower().endswith("."+bad) for x in matches)
|
|
275
|
+
E["exact_fp"] += fp
|
|
276
|
+
E["zero"] += zero
|
|
277
|
+
k = E["kinds"][kind]; k[0] += 1; k[1] += fp; k[2] += zero
|
|
278
|
+
print("E done", {k: list(v) for k, v in E["kinds"].items()}, flush=True)
|
|
279
|
+
E["kinds"] = {k: list(v) for k, v in E["kinds"].items()}
|
|
280
|
+
R["E honesty"] = E
|
|
281
|
+
|
|
282
|
+
# ------------------------------------------------------------- F: doc fidelity
|
|
283
|
+
F = {"n":0,"param_names_ok":0,"summary_ok":0,"since_ok":0,"since_present":0,"overload_docs_ok":0,"overload_checked":0}
|
|
284
|
+
fsample = stratified([m for m in methods if m.pnames and m.summary and m.member != "#ctor"], 50)
|
|
285
|
+
for m in fsample:
|
|
286
|
+
try: q = m.short_sig_prefix
|
|
287
|
+
except Exception: continue
|
|
288
|
+
matches, _ = search(q)
|
|
289
|
+
if not matches: continue
|
|
290
|
+
top = matches[0]
|
|
291
|
+
F["n"] += 1
|
|
292
|
+
got_pnames = [p.get("name") for p in (top.get("parameters") or [])]
|
|
293
|
+
F["param_names_ok"] += (got_pnames == m.pnames)
|
|
294
|
+
gs, ws = (top.get("summary") or ""), m.summary
|
|
295
|
+
F["summary_ok"] += (re.sub(r"\W","",gs)[:80] == re.sub(r"\W","",ws)[:80])
|
|
296
|
+
if m.since:
|
|
297
|
+
F["since_present"] += 1
|
|
298
|
+
F["since_ok"] += (top.get("since") == m.since)
|
|
299
|
+
# overload-doc attribution: right param COUNT on the top signature
|
|
300
|
+
if overload_count[(m.typename, m.member)] > 1:
|
|
301
|
+
F["overload_checked"] += 1
|
|
302
|
+
sig = top.get("signature","")
|
|
303
|
+
inner = sig[sig.index("(")+1:sig.rindex(")")] if "(" in sig else ""
|
|
304
|
+
F["overload_docs_ok"] += (len(split_top(inner)) == len(m.params) if inner else len(m.params)==0)
|
|
305
|
+
print("F done", F, flush=True)
|
|
306
|
+
R["F fidelity"] = F
|
|
307
|
+
|
|
308
|
+
# ------------------------------------------------------------------ G: latency
|
|
309
|
+
LAT.sort()
|
|
310
|
+
def pct(p): return LAT[min(len(LAT)-1, int(len(LAT)*p))]
|
|
311
|
+
R["G latency"] = {"queries": len(LAT), "cold_first_ms": round(LAT[-1] if not LAT else None or 0),
|
|
312
|
+
"p50_ms": round(pct(0.50)), "p95_ms": round(pct(0.95)), "p99_ms": round(pct(0.99)),
|
|
313
|
+
"max_ms": round(LAT[-1])}
|
|
314
|
+
|
|
315
|
+
json.dump(R, open(sys.argv[1] if len(sys.argv)>1 else "benchmark-results.json","w"), indent=1, default=str)
|
|
316
|
+
print("ALL DONE", flush=True)
|
|
317
|
+
print(json.dumps(R, indent=1, default=str)[:3000])
|