pi-revit 0.2.9 → 0.2.11
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 +41 -0
- package/package.json +1 -1
- package/scripts/benchmark-search-docs.py +313 -0
- package/src/Revit/Tools/SearchApiDocs.cs +102 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ Format follows [Keep a Changelog](https://keepachangelog.com/); version headers
|
|
|
7
7
|
Every published version gets an entry with **Added** / **Changed** / **Fixed** sections
|
|
8
8
|
describing what the user will notice — not internal refactors.
|
|
9
9
|
|
|
10
|
+
## [0.2.11] - 2026-07-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `search_api_docs`: signature queries now accept .NET type names and qualified names —
|
|
14
|
+
`Wall.Create(Document, Curve, ElementId, Boolean` and `...(System.String` match the
|
|
15
|
+
rendered `bool` / `string`, and `(Autodesk.Revit.DB.Document` matches `Document`.
|
|
16
|
+
Parameter types in the query are reduced exactly the way the index renders them
|
|
17
|
+
(namespace stripped, CLR name → C# keyword, case-insensitive).
|
|
18
|
+
- `search_api_docs`: Creation-factory calls resolve — `Document.Create.NewRoom(Level, UV`
|
|
19
|
+
finds `Document.NewRoom`, and `Document.Create.NewFamilyInstance` finds the
|
|
20
|
+
`ItemFactoryBase` overloads (factory members documented on a base class). A note in the
|
|
21
|
+
result explains the rewrite. Applies only to the literal `Document.Create.` /
|
|
22
|
+
`Application.Create.` prefixes; ordinary members like `Wall.Create` are untouched, and
|
|
23
|
+
nonsense like `Document.Create.Banana` still honestly returns nothing.
|
|
24
|
+
|
|
25
|
+
Both were observed live: pi stumbled on these five times across two modeling sessions.
|
|
26
|
+
The benchmark gained six regression probes for them.
|
|
27
|
+
|
|
28
|
+
Requires redeploying the Revit add-in (`scripts\deploy.ps1` with Revit closed, then
|
|
29
|
+
restart Revit).
|
|
30
|
+
|
|
31
|
+
## [0.2.10] - 2026-07-22
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- `search_api_docs`: constructor overloads can now be targeted with the natural C#
|
|
35
|
+
spelling — `FilteredElementCollector(Document` matches even though constructors are
|
|
36
|
+
rendered as `new FilteredElementCollector(Document)`. Previously only the `new `-prefixed
|
|
37
|
+
form matched (the same typography-cliff family as the 0.2.8 comma-spacing fix).
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- `scripts/benchmark-search-docs.py`: a reproducible ~630-query benchmark of
|
|
41
|
+
`search_api_docs` scored against Autodesk's own RevitAPI.xml — exact-name recall,
|
|
42
|
+
signature/spacing variants, hard syntax, namespace ambiguity, adversarial honesty
|
|
43
|
+
controls, documentation fidelity, and latency percentiles. Run it against any Revit
|
|
44
|
+
version with the bridge loaded. Measured on Revit 2025 at 0.2.9: 100% recall / 98.8%
|
|
45
|
+
top-1 on exact names, 100% spacing-variant agreement, 0 false positives across 99
|
|
46
|
+
adversarial mutations, 50/50 correct parameter docs, p50 8 ms.
|
|
47
|
+
|
|
48
|
+
Requires redeploying the Revit add-in (`scripts\deploy.ps1` with Revit closed, then
|
|
49
|
+
restart Revit).
|
|
50
|
+
|
|
10
51
|
## [0.2.9] - 2026-07-21
|
|
11
52
|
|
|
12
53
|
### Added
|
package/package.json
CHANGED
|
@@ -0,0 +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])
|
|
@@ -81,7 +81,7 @@ namespace RevitBridge.Tools
|
|
|
81
81
|
if (index.Members.Count == 0)
|
|
82
82
|
throw new InvalidOperationException("The Revit API documentation index is empty. " + string.Join(" ", index.Warnings));
|
|
83
83
|
|
|
84
|
-
var (top, total) = Search(index, query, kindFilter, maxResults);
|
|
84
|
+
var (top, total, rewriteNote) = Search(index, query, kindFilter, maxResults);
|
|
85
85
|
|
|
86
86
|
var matches = new List<Dictionary<string, object?>>(top.Count);
|
|
87
87
|
for (int i = 0; i < top.Count; i++)
|
|
@@ -116,22 +116,56 @@ namespace RevitBridge.Tools
|
|
|
116
116
|
indexedMembers = index.Members.Count,
|
|
117
117
|
sources = index.SourceFiles,
|
|
118
118
|
warnings = index.Warnings.Count > 0 ? index.Warnings : null,
|
|
119
|
-
}, BuildMarkdown(query, top, total, index));
|
|
119
|
+
}, BuildMarkdown(query, top, total, index) + (rewriteNote is null ? string.Empty : $"\nNote: {rewriteNote}"));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// ----------------------------------------------------------------- search
|
|
123
123
|
|
|
124
124
|
private static readonly char[] WordSeparators = { '.', ' ', '_', '(', ')', ',', ':', '-', '/' };
|
|
125
125
|
|
|
126
|
-
private static (List<ApiMember> Top, int Total) Search(DocIndex index, string query, char? kindFilter, int maxResults)
|
|
126
|
+
private static (List<ApiMember> Top, int Total, string? RewriteNote) Search(DocIndex index, string query, char? kindFilter, int maxResults)
|
|
127
127
|
{
|
|
128
128
|
// Signatures are rendered exactly one way ("Name(Type, Type)"): normalize the
|
|
129
129
|
// query's spacing around commas and parentheses so 'Wall.Create(Document,Curve'
|
|
130
130
|
// and 'Wall.Create( Document, Curve' hit the same rendered text instead of
|
|
131
|
-
// failing on typography.
|
|
132
|
-
|
|
131
|
+
// failing on typography. Parameter types are additionally reduced the same way
|
|
132
|
+
// the renderer reduces them (namespaces stripped, CLR names -> C# keywords), so
|
|
133
|
+
// 'Wall.Create(Document, Curve, ElementId, Boolean' and
|
|
134
|
+
// '...(System.String' match the rendered 'bool' / 'string'.
|
|
135
|
+
string q = NormalizeSignatureQuery(query).ToLowerInvariant();
|
|
133
136
|
q = Regex.Replace(q, @"\s*,\s*", ", ");
|
|
134
137
|
q = Regex.Replace(q, @"\(\s+", "(");
|
|
138
|
+
|
|
139
|
+
// The Creation-factory pattern: code says doc.Create.NewRoom(...) but the docs
|
|
140
|
+
// live on Autodesk.Revit.Creation.Document (rendered 'Document.NewRoom') or a
|
|
141
|
+
// base class like ItemFactoryBase. Try the factory rewrite, then the bare
|
|
142
|
+
// member, before giving up. Deterministic rewrites of an exact idiom — never
|
|
143
|
+
// applied unless the literal 'document.create.' / 'application.create.' prefix
|
|
144
|
+
// is present, so ordinary names like Wall.Create are untouched.
|
|
145
|
+
var candidates = new List<(string Query, string? Note)> { (q, null) };
|
|
146
|
+
foreach (string factory in new[] { "document.create.", "application.create." })
|
|
147
|
+
{
|
|
148
|
+
if (!q.StartsWith(factory, StringComparison.Ordinal))
|
|
149
|
+
continue;
|
|
150
|
+
string owner = factory[..(factory.IndexOf('.') + 1)]; // "document."
|
|
151
|
+
string rest = q[factory.Length..];
|
|
152
|
+
candidates.Add((owner + rest, $"'{owner}Create.*' is the Creation factory — matched as '{owner}{rest}'."));
|
|
153
|
+
string bareMember = rest.Split('(')[0];
|
|
154
|
+
if (bareMember.Length > 0)
|
|
155
|
+
candidates.Add((bareMember, $"'{owner}Create.{bareMember}' is a Creation-factory call; its docs live on the factory class (e.g. ItemFactoryBase) — matched by member name '{bareMember}'."));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
foreach (var (candidate, note) in candidates)
|
|
159
|
+
{
|
|
160
|
+
var (top, total) = RunScoring(index, candidate, kindFilter, maxResults);
|
|
161
|
+
if (total > 0)
|
|
162
|
+
return (top, total, note);
|
|
163
|
+
}
|
|
164
|
+
return (new List<ApiMember>(), 0, null);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private static (List<ApiMember> Top, int Total) RunScoring(DocIndex index, string q, char? kindFilter, int maxResults)
|
|
168
|
+
{
|
|
135
169
|
string[] words = q.Split(WordSeparators, StringSplitOptions.RemoveEmptyEntries);
|
|
136
170
|
|
|
137
171
|
var scored = new List<(ApiMember Member, int Score)>();
|
|
@@ -155,6 +189,65 @@ namespace RevitBridge.Tools
|
|
|
155
189
|
return (top, scored.Count);
|
|
156
190
|
}
|
|
157
191
|
|
|
192
|
+
/// <summary>Reduces the parameter part of a signature query exactly the way the
|
|
193
|
+
/// renderer reduces signatures: each identifier keeps only its last dot-segment
|
|
194
|
+
/// and CLR primitive names map to C# keywords (case-insensitively — queries say
|
|
195
|
+
/// 'Boolean' or 'boolean'; signatures render 'bool'). The member path before the
|
|
196
|
+
/// first '(' is left untouched.</summary>
|
|
197
|
+
private static string NormalizeSignatureQuery(string query)
|
|
198
|
+
{
|
|
199
|
+
int paren = query.IndexOf('(');
|
|
200
|
+
if (paren < 0)
|
|
201
|
+
return query;
|
|
202
|
+
|
|
203
|
+
string tail = query[(paren + 1)..];
|
|
204
|
+
var result = new StringBuilder(tail.Length);
|
|
205
|
+
int i = 0;
|
|
206
|
+
while (i < tail.Length)
|
|
207
|
+
{
|
|
208
|
+
char c = tail[i];
|
|
209
|
+
if (char.IsLetter(c) || c == '_')
|
|
210
|
+
{
|
|
211
|
+
int start = i, lastDot = -1;
|
|
212
|
+
while (i < tail.Length && (char.IsLetterOrDigit(tail[i]) || tail[i] is '_' or '.'))
|
|
213
|
+
{
|
|
214
|
+
if (tail[i] == '.')
|
|
215
|
+
lastDot = i;
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
string identifier = tail[(lastDot >= 0 ? lastDot + 1 : start)..i];
|
|
219
|
+
result.Append(QueryTypeAliases.TryGetValue(identifier, out string? keyword) ? keyword : identifier);
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
{
|
|
223
|
+
result.Append(c);
|
|
224
|
+
i++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return query[..(paren + 1)] + result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// <summary>Case-insensitive inverse of MapTypeKeyword for query text.</summary>
|
|
231
|
+
private static readonly Dictionary<string, string> QueryTypeAliases = new(StringComparer.OrdinalIgnoreCase)
|
|
232
|
+
{
|
|
233
|
+
["String"] = "string",
|
|
234
|
+
["Boolean"] = "bool",
|
|
235
|
+
["Int32"] = "int",
|
|
236
|
+
["Int64"] = "long",
|
|
237
|
+
["Int16"] = "short",
|
|
238
|
+
["Double"] = "double",
|
|
239
|
+
["Single"] = "float",
|
|
240
|
+
["Object"] = "object",
|
|
241
|
+
["Void"] = "void",
|
|
242
|
+
["Byte"] = "byte",
|
|
243
|
+
["SByte"] = "sbyte",
|
|
244
|
+
["Char"] = "char",
|
|
245
|
+
["Decimal"] = "decimal",
|
|
246
|
+
["UInt16"] = "ushort",
|
|
247
|
+
["UInt32"] = "uint",
|
|
248
|
+
["UInt64"] = "ulong",
|
|
249
|
+
};
|
|
250
|
+
|
|
158
251
|
private static int Score(ApiMember member, string q, string[] words)
|
|
159
252
|
{
|
|
160
253
|
// A query with a parenthesis targets a specific overload by signature,
|
|
@@ -165,7 +258,11 @@ namespace RevitBridge.Tools
|
|
|
165
258
|
int score;
|
|
166
259
|
if (member.CompositeLower == q) score = 1000;
|
|
167
260
|
else if (signatureQuery && member.SignatureLower == q) score = 980;
|
|
261
|
+
// Constructors render as "new Type(...)": accept the natural C# spelling
|
|
262
|
+
// 'FilteredElementCollector(Document' without requiring the 'new' prefix.
|
|
263
|
+
else if (signatureQuery && member.SignatureLower == "new " + q) score = 975;
|
|
168
264
|
else if (signatureQuery && member.SignatureLower.StartsWith(q, StringComparison.Ordinal)) score = 950;
|
|
265
|
+
else if (signatureQuery && member.SignatureLower.StartsWith("new " + q, StringComparison.Ordinal)) score = 945;
|
|
169
266
|
else if (member.ShortNameLower == q) score = 900;
|
|
170
267
|
else if (member.CompositeLower.StartsWith(q, StringComparison.Ordinal)) score = 700;
|
|
171
268
|
else if (member.ShortNameLower.StartsWith(q, StringComparison.Ordinal)) score = 650;
|