pi-revit 0.5.1 → 0.5.2
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 +11 -2
- package/README.md +802 -803
- package/docs/architecture.md +180 -260
- package/package.json +21 -17
- package/AGENTS.md +0 -167
- package/docs/evaluation.md +0 -434
- package/docs/invariants.json +0 -147
- package/scripts/benchmark-search-docs.py +0 -322
- package/scripts/check-tool-documentation.mjs +0 -287
- package/scripts/generate-contracts.mjs +0 -80
- package/scripts/lib/platform.mjs +0 -226
- package/scripts/test-extension.mjs +0 -15
package/docs/invariants.json
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": 1,
|
|
3
|
-
"description": "Every normative PI-Revit rule and how it is enforced. Guidance sentences carry <!-- inv:<id> --> tags. enforcement=code requires an existing test name (substring of a test title under tests/). A safety rule may be advisory only when it concerns agent intent that code cannot observe; then it must name the agent-eval scenario that measures it. Checked by npm run test:docs.",
|
|
4
|
-
"invariants": [
|
|
5
|
-
{
|
|
6
|
-
"id": "document-identity-guard",
|
|
7
|
-
"rule": "Model writes, previews, exports, view activation and selection changes run only against the exact open document identity supplied by the caller.",
|
|
8
|
-
"severity": "safety", "enforcement": "code",
|
|
9
|
-
"location": ["src/Revit/Tools/DocumentGuard.cs", "src/Revit/ToolRegistry.cs"],
|
|
10
|
-
"test": "registry requires exact identity"
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"id": "no-session-fallback",
|
|
14
|
-
"rule": "Once a Revit session is selected, calls never silently fall back to another session.",
|
|
15
|
-
"severity": "safety", "enforcement": "code",
|
|
16
|
-
"location": ["extensions/pi-revit/instance-router.ts"],
|
|
17
|
-
"test": "selected bridge removal never falls back to a remaining instance"
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"id": "receipt-routing",
|
|
21
|
-
"rule": "Receipt reads and identical retries go to the original bridge session; a retry is never replayed against a different session.",
|
|
22
|
-
"severity": "safety", "enforcement": "code",
|
|
23
|
-
"location": ["extensions/pi-revit/index.ts", "extensions/pi-revit/instance-router.ts"],
|
|
24
|
-
"test": "retry from an earlier bridge generation is rejected before any POST"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"id": "saved-result-sandbox",
|
|
28
|
-
"rule": "read_revit_result resolves only result IDs created in this extension session and never reads arbitrary files.",
|
|
29
|
-
"severity": "safety", "enforcement": "code",
|
|
30
|
-
"location": ["extensions/pi-revit/index.ts"],
|
|
31
|
-
"test": "retrieval rejects unknown IDs and invalid ranges without arbitrary file access"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"id": "manual-path-containment",
|
|
35
|
-
"rule": "Documentation paths are resolved only inside the package; a bridge-provided path is never trusted.",
|
|
36
|
-
"severity": "safety", "enforcement": "code",
|
|
37
|
-
"location": ["extensions/pi-revit/tool-documentation.ts"],
|
|
38
|
-
"test": "realpath containment rejects a manual link escaping its documentation root"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"id": "no-tool-saves-model",
|
|
42
|
-
"rule": "No bridge tool saves, closes or synchronizes the model; a committed transaction is not a file save.",
|
|
43
|
-
"severity": "safety", "enforcement": "code",
|
|
44
|
-
"location": ["src/Revit/Tools/"],
|
|
45
|
-
"test": "architecture: no bridge tool saves, closes or synchronizes a document"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"id": "parameter-ambiguity",
|
|
49
|
-
"rule": "A display-name parameter reference that matches several parameters on one element is never silently resolved to one of them.",
|
|
50
|
-
"severity": "safety", "enforcement": "code",
|
|
51
|
-
"location": ["src/Revit/Tools/ParameterResolver.cs"],
|
|
52
|
-
"test": "ambiguous display name is rejected for writes and filters"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"id": "missing-not-silent",
|
|
56
|
-
"rule": "A filter naming a parameter that is missing on probed elements keeps its warning even when other elements match.",
|
|
57
|
-
"severity": "correctness", "enforcement": "code",
|
|
58
|
-
"location": ["src/Revit/Tools/GetElements.cs"],
|
|
59
|
-
"test": "missing-parameter warning survives other matches"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"id": "special-objects-flagged",
|
|
63
|
-
"rule": "System-owned objects such as titleblock revision schedules are never mixed silently into ordinary results; they are flagged or excluded with a count.",
|
|
64
|
-
"severity": "correctness", "enforcement": "code",
|
|
65
|
-
"location": ["src/Revit/Tools/ElementTraits.cs", "src/Revit/Tools/ManageSheetPlacements.cs"],
|
|
66
|
-
"test": "titleblock revision schedules are flagged in sheet placement listings"
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
"id": "derived-state-reported",
|
|
70
|
-
"rule": "A tool that creates an object from an existing one (duplicate, copy, mirror, retype) reports the state the object inherited: hidden categories and elements, filters, overrides, template and carried values.",
|
|
71
|
-
"severity": "correctness", "enforcement": "code",
|
|
72
|
-
"location": ["src/Revit/Tools/InheritedState.cs", "src/Revit/Tools/InheritedState.Summary.cs", "scripts/check-tool-documentation.mjs"],
|
|
73
|
-
"test": "architecture: a tool that duplicates, copies or retypes reports inherited state",
|
|
74
|
-
"eval_scenario": "modify-duplicate-hidden-view"
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
"id": "model-changes-reported",
|
|
78
|
-
"rule": "Every call of a tool that can change the model reports the objects it added, modified and deleted, and the visibility state of new views, without tool-specific code.",
|
|
79
|
-
"severity": "correctness", "enforcement": "code",
|
|
80
|
-
"location": ["src/Revit/Tools/ModelChanges.cs", "src/Revit/Tools/ChangeSet.cs", "src/Revit/BridgeServer.cs"],
|
|
81
|
-
"test": "preview rollback leaves no net change"
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
"id": "existing-objects-not-reused",
|
|
85
|
-
"rule": "An object that existed before the request is not edited, reused, replaced or deleted in place of one the request asks to create: names already in use are rejected with the existing object's identity, a change to a pre-existing object the request names is flagged, and the agent asks or reports.",
|
|
86
|
-
"severity": "safety", "enforcement": "code",
|
|
87
|
-
"location": ["src/Revit/Tools/ElementNames.cs", "extensions/pi-revit/scope-monitor.ts", "extensions/pi-revit/platform-prompt.ts"],
|
|
88
|
-
"test": "a write to a pre-existing object named in the request adds a scope note",
|
|
89
|
-
"eval_scenario": "modify-name-collision"
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"id": "document-kind-declared",
|
|
93
|
-
"rule": "Every bridge tool declares the document kinds it works in (project, family); the bridge refuses any other kind before the tool runs, naming the route to use instead.",
|
|
94
|
-
"severity": "correctness", "enforcement": "code",
|
|
95
|
-
"location": ["src/Revit/Tools/ToolContract.cs", "src/Revit/Tools/DocumentGuard.cs", "src/Revit/BridgeServer.cs", "scripts/check-tool-documentation.mjs"],
|
|
96
|
-
"test": "a project-only tool refuses a family document with the family API route"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
"id": "limits-have-alternatives",
|
|
100
|
-
"rule": "Every declared tool limit names an alternative route, and discovery without a full match returns the capability route instead of an unexplained absence.",
|
|
101
|
-
"severity": "correctness", "enforcement": "code",
|
|
102
|
-
"location": ["src/Revit/Tools/ToolContract.cs", "extensions/pi-revit/tool-catalog.ts", "scripts/check-tool-documentation.mjs"],
|
|
103
|
-
"test": "unmatched query returns the capability route"
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
"id": "linked-ids-stay-linked",
|
|
107
|
-
"rule": "Linked-element IDs identify elements in their linked document and must not be passed to host write or selection tools.",
|
|
108
|
-
"severity": "safety", "enforcement": "advisory",
|
|
109
|
-
"reason": "A linked element ID is an integer that can also exist in the host; the bridge cannot tell which document the caller meant.",
|
|
110
|
-
"eval_scenario": "linked-ids-not-used-for-host-writes"
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
"id": "identity-refresh-intent",
|
|
114
|
-
"rule": "After an identity rejection, establish that the intended model is active before refreshing the ID; never adopt another model's ID to get past the guard.",
|
|
115
|
-
"severity": "safety", "enforcement": "advisory",
|
|
116
|
-
"reason": "The guard rejects mismatches, but only the agent knows which model the user meant.",
|
|
117
|
-
"eval_scenario": "stale-identity-recovery"
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"id": "uncertain-outcome",
|
|
121
|
-
"rule": "An unknown receipt, unavailable bridge or restart does not prove an action never ran; inspect state before any new modification.",
|
|
122
|
-
"severity": "safety", "enforcement": "advisory",
|
|
123
|
-
"reason": "Whether a new call is a duplicate depends on the agent's reading of state the bridge no longer holds.",
|
|
124
|
-
"eval_scenario": "timeout-recovery"
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
"id": "capability-claims-checked",
|
|
128
|
-
"rule": "Say an operation is not possible only after checking dedicated tools, declared limits and the API route, naming what was checked.",
|
|
129
|
-
"severity": "correctness", "enforcement": "advisory",
|
|
130
|
-
"reason": "Agent reasoning; supported by the platform protocol and discovery fallback, measured by evaluation.",
|
|
131
|
-
"eval_scenario": "boundary-manual-limit"
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"id": "completion-proportionate",
|
|
135
|
-
"rule": "Do only the requested change, verify it with the declared method, then stop and report; offer further improvements instead of making them.",
|
|
136
|
-
"severity": "correctness", "enforcement": "advisory",
|
|
137
|
-
"reason": "Agent behavior; the extension steers repeated verify-and-edit loops but cannot judge the request.",
|
|
138
|
-
"eval_scenario": "modify-perspective-view"
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
"id": "workspace-root-clean",
|
|
142
|
-
"rule": "Never create files loose in the workspace root.",
|
|
143
|
-
"severity": "guidance", "enforcement": "advisory",
|
|
144
|
-
"reason": "Workspace convention for the agent's own file writes."
|
|
145
|
-
}
|
|
146
|
-
]
|
|
147
|
-
}
|
|
@@ -1,322 +0,0 @@
|
|
|
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, kind=None):
|
|
35
|
-
args = {"query": query, "max_results": max_results}
|
|
36
|
-
if kind: args["kind"] = kind
|
|
37
|
-
body = json.dumps(args).encode("utf-8")
|
|
38
|
-
url = f"{BASE}/tools/search_api_docs/execute?token={TOKEN}&timeout_ms=60000"
|
|
39
|
-
req = urllib.request.Request(url, data=body, headers={"content-type": "application/json"}, method="POST")
|
|
40
|
-
t0 = time.perf_counter()
|
|
41
|
-
with urllib.request.urlopen(req, timeout=90) as resp:
|
|
42
|
-
payload = json.load(resp)
|
|
43
|
-
LAT.append((time.perf_counter() - t0) * 1000)
|
|
44
|
-
det = (payload.get("details") or {}).get("payload") or {}
|
|
45
|
-
return det.get("matches") or [], det.get("totalMatches", 0)
|
|
46
|
-
|
|
47
|
-
# ---------------------------------------------------------------- ground truth
|
|
48
|
-
PRIM = {"System.String": "string", "System.Boolean": "bool", "System.Double": "double",
|
|
49
|
-
"System.Int32": "int", "System.Int64": "long", "System.Object": "object",
|
|
50
|
-
"System.Byte": "byte", "System.Single": "float", "System.UInt32": "uint"}
|
|
51
|
-
|
|
52
|
-
def split_top(params): # split on commas outside {}
|
|
53
|
-
out, depth, cur = [], 0, ""
|
|
54
|
-
for ch in params:
|
|
55
|
-
if ch == "{": depth += 1
|
|
56
|
-
elif ch == "}": depth -= 1
|
|
57
|
-
if ch == "," and depth == 0: out.append(cur); cur = ""
|
|
58
|
-
else: cur += ch
|
|
59
|
-
if cur: out.append(cur)
|
|
60
|
-
return out
|
|
61
|
-
|
|
62
|
-
def short_type(t):
|
|
63
|
-
t = t.strip()
|
|
64
|
-
byref = t.endswith("@")
|
|
65
|
-
if byref: t = t[:-1]
|
|
66
|
-
array = ""
|
|
67
|
-
while t.endswith("[]"): array += "[]"; t = t[:-2]
|
|
68
|
-
if t in PRIM: base = PRIM[t]
|
|
69
|
-
elif "{" in t:
|
|
70
|
-
outer, inner = t[:t.index("{")], t[t.index("{")+1:t.rindex("}")]
|
|
71
|
-
outer = outer.split(".")[-1].split("`")[0]
|
|
72
|
-
base = f"{outer}<{', '.join(short_type(x) for x in split_top(inner))}>"
|
|
73
|
-
else:
|
|
74
|
-
base = t.split(".")[-1]
|
|
75
|
-
return base + array + ("&" if byref else "")
|
|
76
|
-
|
|
77
|
-
class Member:
|
|
78
|
-
__slots__ = ("kind","full","typename","member","params","summary","pnames","since")
|
|
79
|
-
def __init__(s, kind, full, typename, member, params, summary, pnames, since):
|
|
80
|
-
s.kind, s.full, s.typename, s.member = kind, full, typename, member
|
|
81
|
-
s.params, s.summary, s.pnames, s.since = params, summary, pnames, since
|
|
82
|
-
@property
|
|
83
|
-
def short_sig_prefix(s):
|
|
84
|
-
shorts = [short_type(p) for p in s.params]
|
|
85
|
-
head = s.typename if s.member == "#ctor" else f"{s.typename}.{s.member}"
|
|
86
|
-
return f"{head}(" + ", ".join(shorts)
|
|
87
|
-
|
|
88
|
-
def text_of(el):
|
|
89
|
-
if el is None: return None
|
|
90
|
-
return re.sub(r"\s+", " ", "".join(el.itertext())).strip() or None
|
|
91
|
-
|
|
92
|
-
print("parsing RevitAPI.xml ...", flush=True)
|
|
93
|
-
root = ET.parse(XML_PATH).getroot()
|
|
94
|
-
methods, props, types = [], [], []
|
|
95
|
-
for m in root.iter("member"):
|
|
96
|
-
name = m.get("name", "")
|
|
97
|
-
summary = text_of(m.find("summary"))
|
|
98
|
-
since = text_of(m.find("since"))
|
|
99
|
-
pnames = [p.get("name") for p in m.findall("param")]
|
|
100
|
-
if name.startswith("M:"):
|
|
101
|
-
body = name[2:]
|
|
102
|
-
params = ""
|
|
103
|
-
if "(" in body:
|
|
104
|
-
body, params = body[:body.index("(")], body[body.index("(")+1:body.rindex(")")]
|
|
105
|
-
parts = body.split(".")
|
|
106
|
-
if len(parts) < 2: continue
|
|
107
|
-
member, typename = parts[-1], parts[-2]
|
|
108
|
-
if "`" in typename or "`" in member: continue
|
|
109
|
-
full = ".".join(parts)
|
|
110
|
-
methods.append(Member("M", full, typename, member, split_top(params) if params else [], summary, pnames, since))
|
|
111
|
-
elif name.startswith("P:"):
|
|
112
|
-
parts = name[2:].split(".")
|
|
113
|
-
if len(parts) < 2 or "`" in parts[-2]: continue
|
|
114
|
-
props.append(Member("P", name[2:], parts[-2], parts[-1], [], summary, pnames, since))
|
|
115
|
-
elif name.startswith("T:"):
|
|
116
|
-
parts = name[2:].split(".")
|
|
117
|
-
if "`" in parts[-1]: continue
|
|
118
|
-
types.append(Member("T", name[2:], parts[-1], "", [], summary, pnames, since))
|
|
119
|
-
|
|
120
|
-
print(f"golden pool: {len(methods)} methods, {len(props)} properties, {len(types)} types", flush=True)
|
|
121
|
-
|
|
122
|
-
by_ns = defaultdict(list)
|
|
123
|
-
for mm in methods: by_ns[".".join(mm.full.split(".")[:-2])].append(mm)
|
|
124
|
-
real_methods = {(m.typename, m.member) for m in methods}
|
|
125
|
-
overload_count = defaultdict(int)
|
|
126
|
-
for m in methods: overload_count[(m.typename, m.member)] += 1
|
|
127
|
-
|
|
128
|
-
def stratified(pool, n):
|
|
129
|
-
groups = defaultdict(list)
|
|
130
|
-
for m in pool: groups[".".join(m.full.split(".")[:4])].append(m)
|
|
131
|
-
out, keys = [], sorted(groups)
|
|
132
|
-
while len(out) < n and keys:
|
|
133
|
-
for k in list(keys):
|
|
134
|
-
if groups[k]: out.append(groups[k].pop(random.randrange(len(groups[k]))))
|
|
135
|
-
else: keys.remove(k)
|
|
136
|
-
if len(out) >= n: break
|
|
137
|
-
return out
|
|
138
|
-
|
|
139
|
-
R = {} # results
|
|
140
|
-
|
|
141
|
-
# ---------------------------------------------------------- A: exact-name recall
|
|
142
|
-
A = {"n":0,"top1":0,"top3":0,"recall":0}
|
|
143
|
-
sampleA = stratified(methods, 90) + stratified(props, 40) + stratified(types, 30)
|
|
144
|
-
for m in sampleA:
|
|
145
|
-
q = f"{m.typename}.{m.member}" if m.kind in "MP" else m.typename
|
|
146
|
-
matches, _ = search(q)
|
|
147
|
-
target = m.full if m.kind in "MP" else m.full
|
|
148
|
-
names = [x.get("name","") for x in matches]
|
|
149
|
-
A["n"] += 1
|
|
150
|
-
A["recall"] += any(n == target for n in names)
|
|
151
|
-
A["top1"] += bool(names and names[0] == target)
|
|
152
|
-
A["top3"] += any(n == target for n in names[:3])
|
|
153
|
-
print("A done", A, flush=True)
|
|
154
|
-
R["A exact-name"] = A
|
|
155
|
-
|
|
156
|
-
# ------------------------------------------------- B: signatures + spacing variants
|
|
157
|
-
B = {"n":0,"top1_sig":0,"variants_agree":0,"variant_sets":0}
|
|
158
|
-
sigpool = [m for m in methods if 2 <= len(m.params) <= 5]
|
|
159
|
-
sampleB = stratified(sigpool, 60)
|
|
160
|
-
for m in sampleB:
|
|
161
|
-
try: prefix = m.short_sig_prefix
|
|
162
|
-
except Exception: continue
|
|
163
|
-
canonical = prefix # "Type.Member(P1, P2..."
|
|
164
|
-
matches, _ = search(canonical)
|
|
165
|
-
B["n"] += 1
|
|
166
|
-
sigs = [x.get("signature","") for x in matches]
|
|
167
|
-
want = f"new {m.typename}(" if m.member == "#ctor" else f"{m.typename}.{m.member}("
|
|
168
|
-
ok = bool(sigs and sigs[0].startswith(want))
|
|
169
|
-
B["top1_sig"] += ok
|
|
170
|
-
variants = [canonical.replace(", ", ","),
|
|
171
|
-
canonical.replace(", ", " , "),
|
|
172
|
-
canonical.replace(", ", ", "),
|
|
173
|
-
canonical.replace("(", "( ", 1)]
|
|
174
|
-
counts = {len(matches)}
|
|
175
|
-
for v in variants:
|
|
176
|
-
vm, _ = search(v)
|
|
177
|
-
counts.add(len(vm))
|
|
178
|
-
B["variant_sets"] += 1
|
|
179
|
-
B["variants_agree"] += (len(counts) == 1)
|
|
180
|
-
print("B done", B, flush=True)
|
|
181
|
-
R["B signature+spacing"] = B
|
|
182
|
-
|
|
183
|
-
# --------------------------------------------------------- C: complex syntax shapes
|
|
184
|
-
C = {"cases": []}
|
|
185
|
-
def probe(label, q, expect_nonzero=True, kind=None):
|
|
186
|
-
try: matches, total = search(q, kind=kind)
|
|
187
|
-
except Exception as e:
|
|
188
|
-
C["cases"].append((label, q, f"ERROR {e}")); return
|
|
189
|
-
ok = (total > 0) if expect_nonzero else (total == 0)
|
|
190
|
-
top = matches[0]["signature"] if matches else "-"
|
|
191
|
-
C["cases"].append((label, q, f"{'OK' if ok else 'MISS'} total={total} top={top[:70]}"))
|
|
192
|
-
|
|
193
|
-
arr = next((m for m in methods if any(p.endswith("[]") for p in m.params)), None)
|
|
194
|
-
if arr: probe("array param", arr.short_sig_prefix)
|
|
195
|
-
nest = next((m for m in methods if any("{" in p and "{" in p[p.index("{")+1:] for p in m.params)), None)
|
|
196
|
-
if nest: probe("nested generic", nest.short_sig_prefix)
|
|
197
|
-
byref = next((m for m in methods if any(p.endswith("@") for p in m.params)), None)
|
|
198
|
-
if byref: probe("ref/out param", byref.short_sig_prefix)
|
|
199
|
-
ctor = next((m for m in methods if m.member == "#ctor" and m.typename == "FilteredElementCollector"), None)
|
|
200
|
-
probe("constructor", "FilteredElementCollector(Document")
|
|
201
|
-
probe("property", "PDFExportOptions.FileName")
|
|
202
|
-
probe("fully qualified type", "Autodesk.Revit.DB.Document")
|
|
203
|
-
probe("case-insensitive", "wall.create(document, curve")
|
|
204
|
-
probe("Double vs double", "UnitUtils.ConvertToInternalUnits(Double, ForgeTypeId")
|
|
205
|
-
probe("trailing garbage brackets", "Wall.Create(Document, Curve]]", True) # observational
|
|
206
|
-
probe("double periods", "Wall..Create", True) # observational
|
|
207
|
-
# 0.2.11: CLR alias + namespace reduction in signature queries, Creation-factory rewrite
|
|
208
|
-
probe("CLR alias Boolean", "Wall.Create(Document, Curve, ElementId, Boolean")
|
|
209
|
-
probe("qualified param type", "Wall.Create(Autodesk.Revit.DB.Document, Curve")
|
|
210
|
-
probe("System-qualified alias", "ParameterFilterElement.Create(Document, System.String")
|
|
211
|
-
probe("factory rewrite", "Document.Create.NewRoom(Level, UV")
|
|
212
|
-
probe("factory inherited member", "Document.Create.NewFamilyInstance")
|
|
213
|
-
probe("factory honesty control", "Document.Create.Banana", expect_nonzero=False)
|
|
214
|
-
# 0.2.14: C# accessor spelling (get_X/set_X) resolves to the documented property/indexer
|
|
215
|
-
probe("accessor indexer", "Element.get_Parameter(BuiltInParameter")
|
|
216
|
-
probe("accessor property", "Element.get_BoundingBox(View")
|
|
217
|
-
probe("literal get_ member", "LocationCurve.get_ElementsAtJoin")
|
|
218
|
-
# 0.2.16: kind=method on an accessor query admits the documented property
|
|
219
|
-
probe("accessor with kind=method", "Element.get_Parameter(BuiltInParameter", kind="method")
|
|
220
|
-
probe("kind=method control (real method)", "Wall.Create(Document, Curve", kind="method")
|
|
221
|
-
print("C done", flush=True)
|
|
222
|
-
R["C complex-syntax"] = C
|
|
223
|
-
|
|
224
|
-
# ------------------------------------------------------------ D: namespace ambiguity
|
|
225
|
-
D = {"cases": []}
|
|
226
|
-
def ns_case(q, want_substr):
|
|
227
|
-
matches, total = search(q)
|
|
228
|
-
top = matches[0]["name"] if matches else "-"
|
|
229
|
-
D["cases"].append((q, want_substr, top, "OK" if want_substr in top else "MISS"))
|
|
230
|
-
for q, want in [
|
|
231
|
-
("Autodesk.Revit.DB.Architecture.Room", ".Architecture.Room"),
|
|
232
|
-
("Autodesk.Revit.DB.Mechanical.Space", ".Mechanical.Space"),
|
|
233
|
-
("Architecture.Room", ".Architecture.Room"),
|
|
234
|
-
("Mechanical.Duct", ".Mechanical.Duct"),
|
|
235
|
-
("Plumbing.Pipe", ".Plumbing.Pipe"),
|
|
236
|
-
("Electrical.Wire", ".Electrical.Wire"),
|
|
237
|
-
("Structure.Rebar", ".Structure.Rebar"),
|
|
238
|
-
("Room", "Room"), ("Space", "Space"), ("Area", "Area"),
|
|
239
|
-
("Connector", "Connector"),
|
|
240
|
-
]: ns_case(q, want)
|
|
241
|
-
print("D done", flush=True)
|
|
242
|
-
R["D namespace"] = D
|
|
243
|
-
|
|
244
|
-
# ------------------------------------------------------- E: honesty / adversarial
|
|
245
|
-
E = {"n":0,"exact_fp":0,"zero":0,"kinds":defaultdict(lambda:[0,0,0])}
|
|
246
|
-
adv = []
|
|
247
|
-
pool = stratified([m for m in methods if 1 <= len(m.params) <= 4 and m.member != "#ctor"], 120)
|
|
248
|
-
for m in pool[:30]: # misspelled member (drop 2nd letter)
|
|
249
|
-
if len(m.member) < 5: continue
|
|
250
|
-
adv.append(("misspelled-member", f"{m.typename}.{m.member[0]+m.member[2:]}", m))
|
|
251
|
-
for m in pool[30:60]: # nonsense parameter
|
|
252
|
-
adv.append(("nonsense-param", f"{m.typename}.{m.member}(Banana", m))
|
|
253
|
-
for m in pool[60:90]: # wrong declaring class
|
|
254
|
-
other = random.choice(types).typename
|
|
255
|
-
if (other, m.member) in real_methods: continue
|
|
256
|
-
adv.append(("wrong-class", f"{other}.{m.member}", (other, m.member)))
|
|
257
|
-
for m in pool[90:120]: # swapped params (only if that order is not a real overload)
|
|
258
|
-
if len(m.params) < 2 or m.params[0] == m.params[1]: continue
|
|
259
|
-
sw = [m.params[1], m.params[0]] + m.params[2:]
|
|
260
|
-
exists = any(o.params[:len(sw)] == sw for o in methods if (o.typename,o.member)==(m.typename,m.member))
|
|
261
|
-
if exists: continue
|
|
262
|
-
shorts = [short_type(p) for p in sw]
|
|
263
|
-
adv.append(("swapped-params", f"{m.typename}.{m.member}(" + ", ".join(shorts), m))
|
|
264
|
-
for kind, q, _meta in adv:
|
|
265
|
-
matches, total = search(q)
|
|
266
|
-
E["n"] += 1
|
|
267
|
-
zero = (total == 0)
|
|
268
|
-
# exact false positive: a returned entry CLAIMS the impossible thing exists
|
|
269
|
-
fp = False
|
|
270
|
-
if kind == "wrong-class":
|
|
271
|
-
t, mem = _meta
|
|
272
|
-
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)
|
|
273
|
-
elif kind == "swapped-params":
|
|
274
|
-
fp = any(x.get("signature","").lower() == (q.lower()+")") for x in matches)
|
|
275
|
-
elif kind == "nonsense-param":
|
|
276
|
-
fp = any("banana" in x.get("signature","").lower() for x in matches)
|
|
277
|
-
elif kind == "misspelled-member":
|
|
278
|
-
bad = q.split(".")[-1].lower()
|
|
279
|
-
fp = any(x.get("name","").lower().endswith("."+bad) for x in matches)
|
|
280
|
-
E["exact_fp"] += fp
|
|
281
|
-
E["zero"] += zero
|
|
282
|
-
k = E["kinds"][kind]; k[0] += 1; k[1] += fp; k[2] += zero
|
|
283
|
-
print("E done", {k: list(v) for k, v in E["kinds"].items()}, flush=True)
|
|
284
|
-
E["kinds"] = {k: list(v) for k, v in E["kinds"].items()}
|
|
285
|
-
R["E honesty"] = E
|
|
286
|
-
|
|
287
|
-
# ------------------------------------------------------------- F: doc fidelity
|
|
288
|
-
F = {"n":0,"param_names_ok":0,"summary_ok":0,"since_ok":0,"since_present":0,"overload_docs_ok":0,"overload_checked":0}
|
|
289
|
-
fsample = stratified([m for m in methods if m.pnames and m.summary and m.member != "#ctor"], 50)
|
|
290
|
-
for m in fsample:
|
|
291
|
-
try: q = m.short_sig_prefix
|
|
292
|
-
except Exception: continue
|
|
293
|
-
matches, _ = search(q)
|
|
294
|
-
if not matches: continue
|
|
295
|
-
top = matches[0]
|
|
296
|
-
F["n"] += 1
|
|
297
|
-
got_pnames = [p.get("name") for p in (top.get("parameters") or [])]
|
|
298
|
-
F["param_names_ok"] += (got_pnames == m.pnames)
|
|
299
|
-
gs, ws = (top.get("summary") or ""), m.summary
|
|
300
|
-
F["summary_ok"] += (re.sub(r"\W","",gs)[:80] == re.sub(r"\W","",ws)[:80])
|
|
301
|
-
if m.since:
|
|
302
|
-
F["since_present"] += 1
|
|
303
|
-
F["since_ok"] += (top.get("since") == m.since)
|
|
304
|
-
# overload-doc attribution: right param COUNT on the top signature
|
|
305
|
-
if overload_count[(m.typename, m.member)] > 1:
|
|
306
|
-
F["overload_checked"] += 1
|
|
307
|
-
sig = top.get("signature","")
|
|
308
|
-
inner = sig[sig.index("(")+1:sig.rindex(")")] if "(" in sig else ""
|
|
309
|
-
F["overload_docs_ok"] += (len(split_top(inner)) == len(m.params) if inner else len(m.params)==0)
|
|
310
|
-
print("F done", F, flush=True)
|
|
311
|
-
R["F fidelity"] = F
|
|
312
|
-
|
|
313
|
-
# ------------------------------------------------------------------ G: latency
|
|
314
|
-
LAT.sort()
|
|
315
|
-
def pct(p): return LAT[min(len(LAT)-1, int(len(LAT)*p))]
|
|
316
|
-
R["G latency"] = {"queries": len(LAT), "cold_first_ms": round(LAT[-1] if not LAT else None or 0),
|
|
317
|
-
"p50_ms": round(pct(0.50)), "p95_ms": round(pct(0.95)), "p99_ms": round(pct(0.99)),
|
|
318
|
-
"max_ms": round(LAT[-1])}
|
|
319
|
-
|
|
320
|
-
json.dump(R, open(sys.argv[1] if len(sys.argv)>1 else "benchmark-results.json","w"), indent=1, default=str)
|
|
321
|
-
print("ALL DONE", flush=True)
|
|
322
|
-
print(json.dumps(R, indent=1, default=str)[:3000])
|