pi-revit 0.4.0 → 0.5.1

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.
Files changed (119) hide show
  1. package/AGENTS.md +167 -0
  2. package/CHANGELOG.md +44 -0
  3. package/README.md +644 -553
  4. package/bin/pi-revit.js +9 -9
  5. package/docs/architecture.md +271 -0
  6. package/docs/evaluation.md +434 -0
  7. package/docs/invariants.json +147 -0
  8. package/extensions/pi-revit/completion-monitor.ts +55 -0
  9. package/extensions/pi-revit/contracts.ts +146 -0
  10. package/extensions/pi-revit/discovery.ts +93 -0
  11. package/extensions/pi-revit/index.ts +342 -255
  12. package/extensions/pi-revit/instance-router.ts +86 -86
  13. package/extensions/pi-revit/platform-prompt.ts +40 -0
  14. package/extensions/pi-revit/scope-monitor.ts +114 -0
  15. package/extensions/pi-revit/script-library.ts +144 -144
  16. package/extensions/pi-revit/tool-catalog.ts +113 -14
  17. package/extensions/pi-revit/tool-documentation.ts +72 -0
  18. package/extensions/pi-revit/tool-schema.ts +8 -0
  19. package/package.json +8 -2
  20. package/scripts/build.ps1 +9 -9
  21. package/scripts/check-sdk.ps1 +66 -66
  22. package/scripts/check-tool-documentation.mjs +287 -0
  23. package/scripts/deploy.ps1 +16 -16
  24. package/scripts/generate-contracts.mjs +80 -0
  25. package/scripts/lib/platform.mjs +226 -0
  26. package/scripts/test-extension.mjs +15 -0
  27. package/skills/pi-revit/SKILL.md +30 -218
  28. package/skills/pi-revit/contracts.generated.json +3524 -0
  29. package/skills/pi-revit/references/execution-rules.md +41 -0
  30. package/skills/pi-revit/references/model-audit-export.md +38 -27
  31. package/skills/pi-revit/references/operation-recovery.md +33 -0
  32. package/skills/pi-revit/references/room-documentation.md +37 -26
  33. package/skills/pi-revit/references/tool-index.md +89 -0
  34. package/skills/pi-revit/references/tools/capture_view.md +62 -0
  35. package/skills/pi-revit/references/tools/change_element_types.md +65 -0
  36. package/skills/pi-revit/references/tools/create_tags.md +85 -0
  37. package/skills/pi-revit/references/tools/delete_elements.md +66 -0
  38. package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
  39. package/skills/pi-revit/references/tools/export_documents.md +75 -0
  40. package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
  41. package/skills/pi-revit/references/tools/get_element_details.md +66 -0
  42. package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
  43. package/skills/pi-revit/references/tools/get_element_types.md +67 -0
  44. package/skills/pi-revit/references/tools/get_elements.md +87 -0
  45. package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
  46. package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
  47. package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
  48. package/skills/pi-revit/references/tools/get_model_health.md +53 -0
  49. package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
  50. package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
  51. package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
  52. package/skills/pi-revit/references/tools/get_schedules.md +71 -0
  53. package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
  54. package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
  55. package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
  56. package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
  57. package/skills/pi-revit/references/tools/manage_selection.md +66 -0
  58. package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
  59. package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
  60. package/skills/pi-revit/references/tools/manage_views.md +95 -0
  61. package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
  62. package/skills/pi-revit/references/tools/open_view.md +59 -0
  63. package/skills/pi-revit/references/tools/ping.md +41 -0
  64. package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
  65. package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
  66. package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
  67. package/skills/pi-revit/references/tools/set_parameters.md +75 -0
  68. package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
  69. package/skills/pi-revit/references/tools/transform_elements.md +79 -0
  70. package/skills/pi-revit/references/visual-verification.md +36 -0
  71. package/skills/pi-revit/tool-manifest.json +338 -0
  72. package/src/Revit/BridgeServer.cs +93 -87
  73. package/src/Revit/OperationStore.cs +178 -178
  74. package/src/Revit/ToolRegistry.cs +88 -57
  75. package/src/Revit/Tools/CaptureView.cs +10 -2
  76. package/src/Revit/Tools/ChangeElementTypes.cs +74 -60
  77. package/src/Revit/Tools/ChangeSet.cs +39 -0
  78. package/src/Revit/Tools/CreateTags.cs +107 -95
  79. package/src/Revit/Tools/DeleteElements.cs +53 -44
  80. package/src/Revit/Tools/DocumentGuard.cs +74 -64
  81. package/src/Revit/Tools/ElementNames.cs +103 -0
  82. package/src/Revit/Tools/ElementQueryScope.cs +27 -27
  83. package/src/Revit/Tools/ElementTraits.cs +53 -0
  84. package/src/Revit/Tools/ExecuteCsharp.cs +54 -45
  85. package/src/Revit/Tools/ExportDocuments.cs +129 -121
  86. package/src/Revit/Tools/GetElementDetails.cs +37 -41
  87. package/src/Revit/Tools/GetElementRelationships.cs +82 -76
  88. package/src/Revit/Tools/GetElementTypes.cs +8 -0
  89. package/src/Revit/Tools/GetElements.cs +75 -86
  90. package/src/Revit/Tools/GetLinkedElements.cs +89 -82
  91. package/src/Revit/Tools/GetLinkedModels.cs +73 -66
  92. package/src/Revit/Tools/GetModelCoordinates.cs +56 -49
  93. package/src/Revit/Tools/GetModelHealth.cs +7 -0
  94. package/src/Revit/Tools/GetModelOverview.cs +185 -158
  95. package/src/Revit/Tools/GetScheduleFields.cs +44 -37
  96. package/src/Revit/Tools/GetSchedules.cs +96 -89
  97. package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
  98. package/src/Revit/Tools/InheritedState.cs +144 -0
  99. package/src/Revit/Tools/ManageElementSets.cs +114 -106
  100. package/src/Revit/Tools/ManageSchedules.cs +174 -164
  101. package/src/Revit/Tools/ManageSelection.cs +45 -37
  102. package/src/Revit/Tools/ManageSheetPlacements.cs +113 -97
  103. package/src/Revit/Tools/ManageSheets.cs +72 -63
  104. package/src/Revit/Tools/ManageViews.cs +115 -100
  105. package/src/Revit/Tools/MeasureGeometry.cs +60 -54
  106. package/src/Revit/Tools/ModelChanges.cs +154 -0
  107. package/src/Revit/Tools/ModelEditBatch.cs +105 -102
  108. package/src/Revit/Tools/ModelEditInputs.cs +49 -49
  109. package/src/Revit/Tools/OpenView.cs +9 -2
  110. package/src/Revit/Tools/ParameterResolver.cs +94 -0
  111. package/src/Revit/Tools/QuerySpatialElements.cs +70 -63
  112. package/src/Revit/Tools/SearchApiDocs.cs +72 -4
  113. package/src/Revit/Tools/SetParameters.cs +60 -79
  114. package/src/Revit/Tools/SpatialBounds.cs +30 -30
  115. package/src/Revit/Tools/SummarizeElements.cs +94 -87
  116. package/src/Revit/Tools/ToolContract.cs +48 -0
  117. package/src/Revit/Tools/ToolSupport.cs +5 -1
  118. package/src/Revit/Tools/TransformElements.cs +72 -57
  119. package/workspace/AGENTS.md +26 -20
@@ -20,8 +20,16 @@ namespace RevitBridge.Tools
20
20
  private const double Epsilon = 1e-6;
21
21
 
22
22
  public string Name => "get_elements";
23
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
24
+ public IReadOnlyList<string> Keywords => new[] { "list", "find", "query", "count", "filter", "search elements", "how many" };
25
+ public IReadOnlyList<ToolLimit> Limits => new[]
26
+ {
27
+ new ToolLimit("Elements inside linked models", "tool", "get_linked_elements"),
28
+ new ToolLimit("Grouped counts or statistics over a whole scope", "tool", "summarize_elements"),
29
+ new ToolLimit("Spatial containment or intersection", "tool", "query_spatial_elements"),
30
+ };
23
31
  public string Label => "Get Elements";
24
- public string Description => "Query Revit elements: scope by category (display name like 'Walls' or enum name like 'OST_Walls'), element class, level, type id, or active view; filter by parameter rules; paginate with offset/limit or just count with count_only. Returns identity fields and optional parameter_names projections; include_type_parameters also reads the requested values from each type. Rules with an explicit BuiltInParameter or shared GUID can evaluate inside Revit's collector; display-name rules, regex rules, and unsupported quick filters run as a per-element scan. A display name can identify different parameters even within one category or class. Scope queries to reduce the scan, or use an explicit parameter identity when appropriate. Numeric rule values are interpreted in the document's display units for that parameter unless 'unit' is given.";
32
+ public string Description => "Query Revit elements: scope by category (display name like 'Walls' or enum name like 'OST_Walls'), element class, level, type id, or active view; filter by parameter rules; paginate with offset/limit or just count with count_only. Returns identity fields and optional parameter_names projections; include_type_parameters also reads the requested values from each type. Rules with an explicit BuiltInParameter or shared GUID can evaluate inside Revit's collector; display-name rules, regex rules, and unsupported quick filters run as a per-element scan. A display name can identify different parameters even within one category or class. Scope queries to reduce the scan, or use an explicit parameter identity when appropriate. Numeric rule values are interpreted in the document's display units for that parameter unless 'unit' is given.";
25
33
 
26
34
  public object ParametersSchema => new
27
35
  {
@@ -72,13 +80,13 @@ namespace RevitBridge.Tools
72
80
  level = new { type = "string", description = "Only elements hosted on this level (level name or id)." },
73
81
  type_id = new { type = "integer", description = "Only instances of this element type id (see get_element_types)." },
74
82
  in_active_view = new { type = "boolean", description = "Only elements visible in the active view. Default false." },
75
- count_only = new { type = "boolean", description = "Return only total_count, no element rows. Default false." },
76
- parameter_names = new
77
- {
78
- type = "array", maxItems = 20, items = new { type = "string" },
79
- description = "Optional parameter projections for each returned element: display names, BuiltInParameter names or guid:<GUID>. Every matching parameter is reported, including missing/ambiguous matches. Numeric values use internal units with separate display values. Ignored by count_only.",
80
- },
81
- include_type_parameters = new { type = "boolean", description = "Also project requested parameters from each element's type. Default false." },
83
+ count_only = new { type = "boolean", description = "Return only total_count, no element rows. Default false." },
84
+ parameter_names = new
85
+ {
86
+ type = "array", maxItems = 20, items = new { type = "string" },
87
+ description = "Optional parameter projections for each returned element: display names, BuiltInParameter names or guid:<GUID>. Every matching parameter is reported, including missing/ambiguous matches. Numeric values use internal units with separate display values. Ignored by count_only.",
88
+ },
89
+ include_type_parameters = new { type = "boolean", description = "Also project requested parameters from each element's type. Default false." },
82
90
  fields = new
83
91
  {
84
92
  type = "array",
@@ -109,10 +117,10 @@ namespace RevitBridge.Tools
109
117
  || string.Equals(JsonArgs.GetString(args, "mode"), "count", StringComparison.OrdinalIgnoreCase);
110
118
  int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
111
119
  int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", DefaultLimit), 1, MaxLimit);
112
- var fields = ResolveFields(args);
113
- var parameterNames = JsonArgs.GetStringArray(args, "parameter_names");
114
- if (parameterNames?.Count > 20) throw new ArgumentException("parameter_names allows at most 20 parameter identities.");
115
- bool includeTypeParameters = JsonArgs.GetBool(args, "include_type_parameters", false);
120
+ var fields = ResolveFields(args);
121
+ var parameterNames = JsonArgs.GetStringArray(args, "parameter_names");
122
+ if (parameterNames?.Count > 20) throw new ArgumentException("parameter_names allows at most 20 parameter identities.");
123
+ bool includeTypeParameters = JsonArgs.GetBool(args, "include_type_parameters", false);
116
124
 
117
125
  ElementId? categoryId = string.IsNullOrWhiteSpace(categoryInput) ? null : CategoryResolver.Resolve(doc, categoryInput);
118
126
  Type? elementClass = string.IsNullOrWhiteSpace(classInput) ? null : ElementClassResolver.Resolve(classInput);
@@ -145,7 +153,7 @@ namespace RevitBridge.Tools
145
153
  return collector;
146
154
  }
147
155
 
148
- var (quickFilter, postPredicate, filterWarnings) = BuildParameterFilter(doc, args, CreateBaseCollector);
156
+ var (quickFilter, postPredicate, filterWarnings, stickyWarnings) = BuildParameterFilter(doc, args, CreateBaseCollector);
149
157
 
150
158
  FilteredElementCollector CreateCollector()
151
159
  {
@@ -158,7 +166,7 @@ namespace RevitBridge.Tools
158
166
  if (countOnly && postPredicate is null)
159
167
  {
160
168
  int quickCount = CreateCollector().GetElementCount();
161
- return CountResult(scope, quickCount, quickCount > 0 ? Array.Empty<string>() : filterWarnings);
169
+ return CountResult(scope, quickCount, quickCount > 0 ? stickyWarnings : filterWarnings);
162
170
  }
163
171
 
164
172
  var rows = countOnly ? null : new List<Dictionary<string, object?>>(Math.Min(limit, 256));
@@ -167,21 +175,22 @@ namespace RevitBridge.Tools
167
175
  {
168
176
  if (postPredicate != null && !postPredicate(element))
169
177
  continue;
170
- if (rows != null && total >= offset && rows.Count < limit)
171
- {
172
- var row = ElementIdentity.Build(doc, element, fields);
173
- if (parameterNames is { Count: > 0 })
174
- row["parameters"] = GetElementDetails.ProjectParameters(doc, element, parameterNames, includeTypeParameters);
175
- rows.Add(row);
176
- }
178
+ if (rows != null && total >= offset && rows.Count < limit)
179
+ {
180
+ var row = ElementIdentity.Build(doc, element, fields);
181
+ if (parameterNames is { Count: > 0 })
182
+ row["parameters"] = GetElementDetails.ProjectParameters(doc, element, parameterNames, includeTypeParameters);
183
+ rows.Add(row);
184
+ }
177
185
  total++;
178
186
  }
179
187
 
180
- // A found match proves the query worked: the not-found-on-probes warning is
188
+ // A found match proves most rules worked: a not-found-on-probes warning is
181
189
  // load-bearing only next to a zero, where it distinguishes 'unknown parameter
182
- // name' from 'no matching elements'. Beside real matches it is just noise.
190
+ // name' from 'no matching elements'. is_empty warnings stay, because a missing
191
+ // parameter can itself produce the matches (inv:missing-not-silent).
183
192
  if (total > 0)
184
- filterWarnings = Array.Empty<string>();
193
+ filterWarnings = stickyWarnings;
185
194
 
186
195
  if (rows is null)
187
196
  return CountResult(scope, total, filterWarnings);
@@ -286,17 +295,18 @@ namespace RevitBridge.Tools
286
295
  public required RuleOp Op { get; init; }
287
296
  public JsonElement Value { get; init; }
288
297
  public string? Unit { get; init; }
298
+ public required ParameterReference Reference { get; init; }
289
299
  public BuiltInParameter? BuiltIn { get; init; }
290
300
  public Guid? SharedGuid { get; init; }
291
301
  public Regex? CompiledRegex { get; init; }
292
302
  public FilterRule? QuickRule { get; set; }
293
303
  }
294
304
 
295
- private static (ElementFilter? Quick, Func<Element, bool>? Post, IReadOnlyList<string> Warnings) BuildParameterFilter(
305
+ private static (ElementFilter? Quick, Func<Element, bool>? Post, IReadOnlyList<string> Warnings, IReadOnlyList<string> StickyWarnings) BuildParameterFilter(
296
306
  Document doc, JsonElement args, Func<FilteredElementCollector> createBaseCollector)
297
307
  {
298
308
  if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty("filter", out var filterElement) || filterElement.ValueKind != JsonValueKind.Object)
299
- return (null, null, Array.Empty<string>());
309
+ return (null, null, Array.Empty<string>(), Array.Empty<string>());
300
310
 
301
311
  bool matchAny = string.Equals(JsonArgs.GetString(filterElement, "match"), "any", StringComparison.OrdinalIgnoreCase);
302
312
  if (!filterElement.TryGetProperty("rules", out var rulesElement) || rulesElement.ValueKind != JsonValueKind.Array)
@@ -304,11 +314,11 @@ namespace RevitBridge.Tools
304
314
 
305
315
  var rules = rulesElement.EnumerateArray().Select(ParseRule).ToList();
306
316
  if (rules.Count == 0)
307
- return (null, null, Array.Empty<string>());
317
+ return (null, null, Array.Empty<string>(), Array.Empty<string>());
308
318
 
309
- // Probe a few in-scope elements for missing-name diagnostics and to find
310
- // storage/spec exemplars for explicit parameter identities. A sample must
311
- // never turn a display name into an assumed globally uniform identity.
319
+ // Probe a few in-scope elements for missing-name diagnostics and to find
320
+ // storage/spec exemplars for explicit parameter identities. A sample must
321
+ // never turn a display name into an assumed globally uniform identity.
312
322
  var probes = new List<Element>(ProbeSize);
313
323
  foreach (Element element in createBaseCollector())
314
324
  {
@@ -325,13 +335,25 @@ namespace RevitBridge.Tools
325
335
  // A warning (not an error: the parameter may exist past the probe window,
326
336
  // and is_empty legitimately matches missing parameters) makes the zero honest.
327
337
  var warnings = new List<string>();
338
+ var stickyWarnings = new List<string>();
328
339
  if (probes.Count > 0)
329
340
  {
330
341
  foreach (var rule in rules)
331
342
  {
332
343
  if (rule.BuiltIn != null || rule.SharedGuid != null)
333
344
  continue;
334
- if (probes.All(probe => FindParameter(probe, rule) is null))
345
+ if (probes.All(probe => FindParameter(probe, rule) is null) && rule.Op == RuleOp.IsEmpty)
346
+ {
347
+ // inv:missing-not-silent: is_empty matches elements that lack the parameter,
348
+ // so a misspelled or localized name matches the whole scope. Keep this warning.
349
+ string sticky = $"Filter parameter '{rule.ParamInput}' was not found on any of the {probes.Count} probed element(s) in scope. "
350
+ + "is_empty also matches elements that do not have the parameter at all, so these results may be elements without it "
351
+ + "rather than elements with an empty value. Check the name, or use the language-independent BuiltInParameter name or guid:<GUID> "
352
+ + "(get_element_details reports builtInParameter per parameter).";
353
+ warnings.Add(sticky);
354
+ stickyWarnings.Add(sticky);
355
+ }
356
+ else if (probes.All(probe => FindParameter(probe, rule) is null))
335
357
  warnings.Add(
336
358
  $"Filter parameter '{rule.ParamInput}' was not found on any of the {probes.Count} probed element(s) in scope, "
337
359
  + "so a 0-match result may mean 'unknown parameter name', not 'no matching elements'. Display names are "
@@ -340,19 +362,19 @@ namespace RevitBridge.Tools
340
362
  }
341
363
  }
342
364
 
343
- foreach (var rule in rules)
344
- {
345
- if (rule.Op is RuleOp.Regex or RuleOp.IsEmpty or RuleOp.IsNotEmpty)
346
- continue; // always post-scan (missing-parameter semantics)
347
- // Category/class scope and unanimous samples do not establish a
348
- // uniform parameter identity across later families. Resolve display
349
- // names per element; only explicit identities may use a quick rule.
350
- if (rule.BuiltIn is null && rule.SharedGuid is null)
351
- continue;
352
- var found = probes.Select(probe => FindParameter(probe, rule)).Where(parameter => parameter != null).ToList();
353
- if (found.Count == 0)
354
- continue;
355
- rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
365
+ foreach (var rule in rules)
366
+ {
367
+ if (rule.Op is RuleOp.Regex or RuleOp.IsEmpty or RuleOp.IsNotEmpty)
368
+ continue; // always post-scan (missing-parameter semantics)
369
+ // Category/class scope and unanimous samples do not establish a
370
+ // uniform parameter identity across later families. Resolve display
371
+ // names per element; only explicit identities may use a quick rule.
372
+ if (rule.BuiltIn is null && rule.SharedGuid is null)
373
+ continue;
374
+ var found = probes.Select(probe => FindParameter(probe, rule)).Where(parameter => parameter != null).ToList();
375
+ if (found.Count == 0)
376
+ continue;
377
+ rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
356
378
  }
357
379
 
358
380
  var quickRules = rules.Where(rule => rule.QuickRule != null).ToList();
@@ -363,10 +385,10 @@ namespace RevitBridge.Tools
363
385
  // OR with any post-scan rule means everything must be post-scanned: a
364
386
  // collector-level OR filter would wrongly exclude post-rule-only matches.
365
387
  if (postRules.Count > 0)
366
- return (null, element => rules.Any(rule => EvaluatePost(doc, element, rule)), warnings);
388
+ return (null, element => rules.Any(rule => EvaluatePost(doc, element, rule)), warnings, stickyWarnings);
367
389
 
368
390
  var filters = quickRules.Select(rule => (ElementFilter)new ElementParameterFilter(rule.QuickRule!)).ToList();
369
- return (filters.Count == 1 ? filters[0] : new LogicalOrFilter(filters), null, warnings);
391
+ return (filters.Count == 1 ? filters[0] : new LogicalOrFilter(filters), null, warnings, stickyWarnings);
370
392
  }
371
393
 
372
394
  ElementFilter? quick = quickRules.Count > 0
@@ -375,7 +397,7 @@ namespace RevitBridge.Tools
375
397
  Func<Element, bool>? post = postRules.Count > 0
376
398
  ? element => postRules.All(rule => EvaluatePost(doc, element, rule))
377
399
  : null;
378
- return (quick, post, warnings);
400
+ return (quick, post, warnings, stickyWarnings);
379
401
  }
380
402
 
381
403
  private static Rule ParseRule(JsonElement element)
@@ -407,26 +429,7 @@ namespace RevitBridge.Tools
407
429
  if (!hasValue && op is not (RuleOp.IsEmpty or RuleOp.IsNotEmpty))
408
430
  throw new ArgumentException($"Filter rule on '{param}' with op {opText} needs a value.");
409
431
 
410
- BuiltInParameter? builtIn = null;
411
- Guid? sharedGuid = null;
412
- if (param.StartsWith("guid:", StringComparison.OrdinalIgnoreCase))
413
- {
414
- sharedGuid = Guid.TryParse(param["guid:".Length..], out var guid)
415
- ? guid
416
- : throw new ArgumentException($"Invalid shared parameter guid: {param}");
417
- }
418
- else
419
- {
420
- string enumName = param.StartsWith("BuiltInParameter.", StringComparison.OrdinalIgnoreCase)
421
- ? param["BuiltInParameter.".Length..]
422
- : param;
423
- // BuiltInParameter names are SHOUTY_SNAKE_CASE; require an underscore or
424
- // all-caps so plain display names like "Comments" never collide.
425
- bool looksLikeEnumName = enumName.Length > 0 && char.IsLetter(enumName[0]) && !enumName.Contains(' ')
426
- && (enumName.Contains('_') || enumName.All(c => !char.IsLetter(c) || char.IsUpper(c)));
427
- if (looksLikeEnumName && Enum.TryParse<BuiltInParameter>(enumName, true, out var parsed) && parsed != BuiltInParameter.INVALID)
428
- builtIn = parsed;
429
- }
432
+ var reference = ParameterReference.Parse(param);
430
433
 
431
434
  Regex? regex = null;
432
435
  if (op == RuleOp.Regex)
@@ -448,29 +451,15 @@ namespace RevitBridge.Tools
448
451
  Op = op,
449
452
  Value = hasValue ? value : default,
450
453
  Unit = JsonArgs.GetString(element, "unit"),
451
- BuiltIn = builtIn,
452
- SharedGuid = sharedGuid,
454
+ Reference = reference,
455
+ BuiltIn = reference.BuiltIn,
456
+ SharedGuid = reference.SharedGuid,
453
457
  CompiledRegex = regex,
454
458
  };
455
459
  }
456
460
 
457
- private static Parameter? FindParameter(Element element, Rule rule)
458
- {
459
- if (rule.BuiltIn is { } builtIn)
460
- return element.get_Parameter(builtIn);
461
- if (rule.SharedGuid is { } guid)
462
- return element.get_Parameter(guid);
463
-
464
- var direct = element.LookupParameter(rule.ParamInput);
465
- if (direct != null)
466
- return direct;
467
- foreach (Parameter parameter in element.Parameters)
468
- {
469
- if (string.Equals(parameter.Definition?.Name, rule.ParamInput, StringComparison.OrdinalIgnoreCase))
470
- return parameter;
471
- }
472
- return null;
473
- }
461
+ /// <summary>Shared resolution policy: missing is null; an ambiguous display name throws with candidates.</summary>
462
+ private static Parameter? FindParameter(Element element, Rule rule) => ParameterResolver.FindSingle(element, rule.Reference);
474
463
 
475
464
  /// <summary>Builds a collector-level FilterRule, or null when the op/storage pair must post-scan.</summary>
476
465
  private static FilterRule? TryBuildQuickRule(Document doc, Rule rule, Parameter exemplar)
@@ -1,82 +1,89 @@
1
- using System.Text.Json;
2
- using System.Text.Json.Nodes;
3
- using Autodesk.Revit.DB;
4
-
5
- namespace RevitBridge.Tools;
6
-
7
- internal sealed class GetLinkedElements : ITool
8
- {
9
- public string Name => "get_linked_elements";
10
- public string Label => "Get Linked Elements";
11
- public string Tier => "advanced";
12
- public string Description => "Query one loaded Revit link with get_elements filters and pagination. Use link_instance_id and expected_linked_document_id from get_linked_models. Returned element IDs belong to the linked document and cannot be passed to host write tools. Optional bounds are axis-aligned in host coordinates, in internal feet. Only direct links are supported; active-view scoping is unavailable.";
13
- public object ParametersSchema
14
- {
15
- get
16
- {
17
- var schema = JsonSerializer.SerializeToNode(new GetElements().ParametersSchema)!.AsObject();
18
- var properties = schema["properties"]!.AsObject();
19
- properties.Remove("in_active_view");
20
- properties["link_instance_id"] = new JsonObject { ["type"] = "integer" };
21
- properties["expected_linked_document_id"] = new JsonObject { ["type"] = "string" };
22
- properties["include_bounds"] = new JsonObject { ["type"] = "boolean" };
23
- schema["required"] = new JsonArray("link_instance_id", "expected_linked_document_id");
24
- return schema;
25
- }
26
- }
27
-
28
- public object Execute(JsonElement args, ToolContext context)
29
- {
30
- var host = context.Document ?? throw new NoActiveDocumentException();
31
- var id = JsonArgs.GetLong(args, "link_instance_id") ?? throw new ArgumentException("link_instance_id is required.");
32
- var link = host.GetElement(new ElementId(id)) as RevitLinkInstance
33
- ?? throw new ArgumentException("link_instance_id is not a Revit link instance in the active host document.");
34
- var linked = link.GetLinkDocument() ?? throw new ArgumentException("The link is unloaded. Load it in Revit before querying it.");
35
- string identity = DocumentGuard.GetIdentity(linked);
36
- if (!string.Equals(JsonArgs.GetString(args, "expected_linked_document_id"), identity, StringComparison.Ordinal))
37
- throw new ArgumentException("The linked document identity is missing or stale. Read get_linked_models again.");
38
- if (JsonArgs.GetBool(args, "in_active_view", false))
39
- throw new ArgumentException("in_active_view is not supported for linked queries.");
40
-
41
- var result = (ToolOutput)new GetElements().Execute(args, new ToolContext(linked, null))!;
42
- var payload = JsonSerializer.SerializeToNode(result.Payload)!.AsObject();
43
- payload["host_document_id"] = DocumentGuard.GetIdentity(host);
44
- payload["linked_document_id"] = identity;
45
- payload["link_instance_id"] = id;
46
- payload["coordinate_unit"] = "ft";
47
- if (payload["elements"] is JsonArray elements)
48
- {
49
- var transform = link.GetTotalTransform();
50
- foreach (var row in elements)
51
- {
52
- var element = linked.GetElement(new ElementId(row!["id"]!.GetValue<long>()));
53
- row["unique_id"] = element.UniqueId;
54
- row["reference"] = new JsonObject
55
- {
56
- ["host_document_id"] = DocumentGuard.GetIdentity(host),
57
- ["link_instance_id"] = id,
58
- ["linked_document_id"] = identity,
59
- ["element_id"] = element.Id.Value,
60
- };
61
- if (JsonArgs.GetBool(args, "include_bounds", false))
62
- row["host_bounds"] = JsonSerializer.SerializeToNode(HostBounds(element.get_BoundingBox(null), transform));
63
- }
64
- }
65
- return new ToolOutput(payload, result.CompactText);
66
- }
67
-
68
- internal static object? HostBounds(BoundingBoxXYZ? box, Transform placement)
69
- {
70
- if (box == null) return null;
71
- var corners = new List<XYZ>(8);
72
- foreach (double x in new[] { box.Min.X, box.Max.X })
73
- foreach (double y in new[] { box.Min.Y, box.Max.Y })
74
- foreach (double z in new[] { box.Min.Z, box.Max.Z })
75
- corners.Add(placement.OfPoint(box.Transform.OfPoint(new XYZ(x, y, z))));
76
- return new
77
- {
78
- min = new[] { corners.Min(p => p.X), corners.Min(p => p.Y), corners.Min(p => p.Z) },
79
- max = new[] { corners.Max(p => p.X), corners.Max(p => p.Y), corners.Max(p => p.Z) },
80
- };
81
- }
82
- }
1
+ using System.Text.Json;
2
+ using System.Text.Json.Nodes;
3
+ using Autodesk.Revit.DB;
4
+
5
+ namespace RevitBridge.Tools;
6
+
7
+ internal sealed class GetLinkedElements : ITool
8
+ {
9
+ public string Name => "get_linked_elements";
10
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.ProjectOnly;
11
+ public IReadOnlyList<string> Keywords => new[] { "linked model", "link", "linked elements", "elements in link", "rvt link", "consultant model" };
12
+ public IReadOnlyList<ToolLimit> Limits => new[]
13
+ {
14
+ new ToolLimit("Nested links", "api", "RevitLinkInstance.GetLinkDocument on the nested link instances"),
15
+ new ToolLimit("Editing linked elements", "user", "Edit the linked model itself"),
16
+ };
17
+ public string Label => "Get Linked Elements";
18
+ public string Tier => "advanced";
19
+ public string Description => "Query one loaded Revit link with get_elements filters and pagination. Use link_instance_id and expected_linked_document_id from get_linked_models. Returned element IDs belong to the linked document and cannot be passed to host write tools. Optional bounds are axis-aligned in host coordinates, in internal feet. Only direct links are supported; active-view scoping is unavailable.";
20
+ public object ParametersSchema
21
+ {
22
+ get
23
+ {
24
+ var schema = JsonSerializer.SerializeToNode(new GetElements().ParametersSchema)!.AsObject();
25
+ var properties = schema["properties"]!.AsObject();
26
+ properties.Remove("in_active_view");
27
+ properties["link_instance_id"] = new JsonObject { ["type"] = "integer" };
28
+ properties["expected_linked_document_id"] = new JsonObject { ["type"] = "string" };
29
+ properties["include_bounds"] = new JsonObject { ["type"] = "boolean" };
30
+ schema["required"] = new JsonArray("link_instance_id", "expected_linked_document_id");
31
+ return schema;
32
+ }
33
+ }
34
+
35
+ public object Execute(JsonElement args, ToolContext context)
36
+ {
37
+ var host = context.Document ?? throw new NoActiveDocumentException();
38
+ var id = JsonArgs.GetLong(args, "link_instance_id") ?? throw new ArgumentException("link_instance_id is required.");
39
+ var link = host.GetElement(new ElementId(id)) as RevitLinkInstance
40
+ ?? throw new ArgumentException("link_instance_id is not a Revit link instance in the active host document.");
41
+ var linked = link.GetLinkDocument() ?? throw new ArgumentException("The link is unloaded. Load it in Revit before querying it.");
42
+ string identity = DocumentGuard.GetIdentity(linked);
43
+ if (!string.Equals(JsonArgs.GetString(args, "expected_linked_document_id"), identity, StringComparison.Ordinal))
44
+ throw new ArgumentException("The linked document identity is missing or stale. Read get_linked_models again.");
45
+ if (JsonArgs.GetBool(args, "in_active_view", false))
46
+ throw new ArgumentException("in_active_view is not supported for linked queries.");
47
+
48
+ var result = (ToolOutput)new GetElements().Execute(args, new ToolContext(linked, null))!;
49
+ var payload = JsonSerializer.SerializeToNode(result.Payload)!.AsObject();
50
+ payload["host_document_id"] = DocumentGuard.GetIdentity(host);
51
+ payload["linked_document_id"] = identity;
52
+ payload["link_instance_id"] = id;
53
+ payload["coordinate_unit"] = "ft";
54
+ if (payload["elements"] is JsonArray elements)
55
+ {
56
+ var transform = link.GetTotalTransform();
57
+ foreach (var row in elements)
58
+ {
59
+ var element = linked.GetElement(new ElementId(row!["id"]!.GetValue<long>()));
60
+ row["unique_id"] = element.UniqueId;
61
+ row["reference"] = new JsonObject
62
+ {
63
+ ["host_document_id"] = DocumentGuard.GetIdentity(host),
64
+ ["link_instance_id"] = id,
65
+ ["linked_document_id"] = identity,
66
+ ["element_id"] = element.Id.Value,
67
+ };
68
+ if (JsonArgs.GetBool(args, "include_bounds", false))
69
+ row["host_bounds"] = JsonSerializer.SerializeToNode(HostBounds(element.get_BoundingBox(null), transform));
70
+ }
71
+ }
72
+ return new ToolOutput(payload, result.CompactText);
73
+ }
74
+
75
+ internal static object? HostBounds(BoundingBoxXYZ? box, Transform placement)
76
+ {
77
+ if (box == null) return null;
78
+ var corners = new List<XYZ>(8);
79
+ foreach (double x in new[] { box.Min.X, box.Max.X })
80
+ foreach (double y in new[] { box.Min.Y, box.Max.Y })
81
+ foreach (double z in new[] { box.Min.Z, box.Max.Z })
82
+ corners.Add(placement.OfPoint(box.Transform.OfPoint(new XYZ(x, y, z))));
83
+ return new
84
+ {
85
+ min = new[] { corners.Min(p => p.X), corners.Min(p => p.Y), corners.Min(p => p.Z) },
86
+ max = new[] { corners.Max(p => p.X), corners.Max(p => p.Y), corners.Max(p => p.Z) },
87
+ };
88
+ }
89
+ }
@@ -1,66 +1,73 @@
1
- using System.Text.Json;
2
- using Autodesk.Revit.DB;
3
-
4
- namespace RevitBridge.Tools;
5
-
6
- internal sealed class GetLinkedModels : ITool
7
- {
8
- public string Name => "get_linked_models";
9
- public string Label => "Get Linked Models";
10
- public string Tier => "advanced";
11
- public string Description => "List placed Revit links, including unloaded instances, exact linked document identities and their transforms into host coordinates. Nested links are not traversed. Coordinates and transform origins use internal feet.";
12
- public object ParametersSchema => new
13
- {
14
- type = "object",
15
- properties = new
16
- {
17
- offset = new { type = "integer", minimum = 0 },
18
- limit = new { type = "integer", minimum = 1, maximum = 1000 },
19
- },
20
- required = Array.Empty<string>(),
21
- };
22
-
23
- public object Execute(JsonElement args, ToolContext context)
24
- {
25
- var doc = context.Document ?? throw new NoActiveDocumentException();
26
- int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
27
- int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 1000);
28
- var links = new FilteredElementCollector(doc).OfClass(typeof(RevitLinkInstance))
29
- .Cast<RevitLinkInstance>().OrderBy(x => x.Id.Value).ToList();
30
- var rows = links.Skip(offset).Take(limit).Select(link =>
31
- {
32
- var linked = link.GetLinkDocument();
33
- var transform = link.GetTotalTransform();
34
- return new
35
- {
36
- link_instance_id = link.Id.Value,
37
- unique_id = link.UniqueId,
38
- name = link.Name,
39
- type_id = link.GetTypeId().Value,
40
- loaded = linked != null,
41
- linked_document_id = linked == null ? null : DocumentGuard.GetIdentity(linked),
42
- document_title = linked?.Title,
43
- transform = new
44
- {
45
- origin = Coordinates(transform.Origin),
46
- basis_x = Coordinates(transform.BasisX),
47
- basis_y = Coordinates(transform.BasisY),
48
- basis_z = Coordinates(transform.BasisZ),
49
- },
50
- };
51
- }).ToArray();
52
- return new
53
- {
54
- document_id = DocumentGuard.GetIdentity(doc),
55
- coordinate_unit = "ft",
56
- total_count = links.Count,
57
- returned_count = rows.Length,
58
- offset,
59
- has_more = offset + rows.Length < links.Count,
60
- next_offset = offset + rows.Length < links.Count ? (int?)(offset + rows.Length) : null,
61
- links = rows,
62
- };
63
- }
64
-
65
- internal static double[] Coordinates(XYZ point) => new[] { point.X, point.Y, point.Z };
66
- }
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class GetLinkedModels : ITool
7
+ {
8
+ public string Name => "get_linked_models";
9
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.ProjectOnly;
10
+ public IReadOnlyList<string> Keywords => new[] { "links", "linked files", "rvt links", "link status", "loaded", "unloaded" };
11
+ public IReadOnlyList<ToolLimit> Limits => new[]
12
+ {
13
+ new ToolLimit("Loading or reloading an unloaded link", "api", "RevitLinkType.Load"),
14
+ new ToolLimit("Nested links", "api", "RevitLinkInstance.GetLinkDocument"),
15
+ };
16
+ public string Label => "Get Linked Models";
17
+ public string Tier => "advanced";
18
+ public string Description => "List placed Revit links, including unloaded instances, exact linked document identities and their transforms into host coordinates. Nested links are not traversed. Coordinates and transform origins use internal feet.";
19
+ public object ParametersSchema => new
20
+ {
21
+ type = "object",
22
+ properties = new
23
+ {
24
+ offset = new { type = "integer", minimum = 0 },
25
+ limit = new { type = "integer", minimum = 1, maximum = 1000 },
26
+ },
27
+ required = Array.Empty<string>(),
28
+ };
29
+
30
+ public object Execute(JsonElement args, ToolContext context)
31
+ {
32
+ var doc = context.Document ?? throw new NoActiveDocumentException();
33
+ int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
34
+ int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 1000);
35
+ var links = new FilteredElementCollector(doc).OfClass(typeof(RevitLinkInstance))
36
+ .Cast<RevitLinkInstance>().OrderBy(x => x.Id.Value).ToList();
37
+ var rows = links.Skip(offset).Take(limit).Select(link =>
38
+ {
39
+ var linked = link.GetLinkDocument();
40
+ var transform = link.GetTotalTransform();
41
+ return new
42
+ {
43
+ link_instance_id = link.Id.Value,
44
+ unique_id = link.UniqueId,
45
+ name = link.Name,
46
+ type_id = link.GetTypeId().Value,
47
+ loaded = linked != null,
48
+ linked_document_id = linked == null ? null : DocumentGuard.GetIdentity(linked),
49
+ document_title = linked?.Title,
50
+ transform = new
51
+ {
52
+ origin = Coordinates(transform.Origin),
53
+ basis_x = Coordinates(transform.BasisX),
54
+ basis_y = Coordinates(transform.BasisY),
55
+ basis_z = Coordinates(transform.BasisZ),
56
+ },
57
+ };
58
+ }).ToArray();
59
+ return new
60
+ {
61
+ document_id = DocumentGuard.GetIdentity(doc),
62
+ coordinate_unit = "ft",
63
+ total_count = links.Count,
64
+ returned_count = rows.Length,
65
+ offset,
66
+ has_more = offset + rows.Length < links.Count,
67
+ next_offset = offset + rows.Length < links.Count ? (int?)(offset + rows.Length) : null,
68
+ links = rows,
69
+ };
70
+ }
71
+
72
+ internal static double[] Coordinates(XYZ point) => new[] { point.X, point.Y, point.Z };
73
+ }