pi-revit 0.4.0 → 0.5.0

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 +465 -430
  3. package/README.md +604 -548
  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
@@ -1,6 +1,6 @@
1
- using System.IO;
2
- using System.Security.Cryptography;
3
- using System.Text;
1
+ using System.IO;
2
+ using System.Security.Cryptography;
3
+ using System.Text;
4
4
  using System.Text.Json;
5
5
  using Autodesk.Revit.DB;
6
6
 
@@ -20,10 +20,18 @@ namespace RevitBridge.Tools
20
20
  private const int PngPixelWidth = 2048;
21
21
 
22
22
  public string Name => "export_documents";
23
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
24
+ public IReadOnlyList<string> Keywords => new[] { "pdf", "dwg", "ifc", "png", "print", "publish", "plot", "export sheets" };
25
+ public IReadOnlyList<ToolLimit> Limits => new[]
26
+ {
27
+ new ToolLimit("Formats other than PDF, DWG, PNG and IFC", "api", "Document.Export overloads (e.g. DXFExportOptions, NavisworksExportOptions); verify the installed exporter"),
28
+ new ToolLimit("Printing to a physical printer", "api", "PrintManager"),
29
+ };
30
+ public string? Verification => "inspect_output";
23
31
  public string Label => "Export Documents";
24
32
  public string Description => "Export documents from the open Revit model. format 'pdf'/'dwg'/'png' export the given sheet/view ids to files: pdf combines everything into one file by default (combine=false writes one PDF per sheet/view, named by Revit's naming rule); png renders 2048 px wide; ifc exports the whole model, or just what one given view shows. Files sort themselves per model: with output_dir omitted they land in Documents\\pi-revit\\Models\\<model title>--<identity hash>\\exports, derived from the document being exported (pass output_dir only for a different explicit target); file_name_prefix sets the base file name (default: the document title; Revit appends view/sheet suffixes for multi-file exports). Returns the produced file paths with sizes. Find sheet/view ids with get_elements (category 'Sheets' or 'Views') first.";
25
- public bool Write => true;
26
- public IReadOnlyList<string> Effects => new[] { "model", "files" };
33
+ public bool Write => true;
34
+ public IReadOnlyList<string> Effects => new[] { "model", "files" };
27
35
  public string Tier => "advanced";
28
36
 
29
37
  public object ParametersSchema => new
@@ -89,37 +97,37 @@ namespace RevitBridge.Tools
89
97
  // recent (a shared output_dir, another tool writing alongside) does not.
90
98
  var before = SnapshotWriteTimes(outputDir);
91
99
 
92
- IReadOnlyList<string> commitWarnings = Array.Empty<string>();
93
- try
94
- {
95
- switch (format)
96
- {
97
- case "pdf": ExportPdf(doc, views, outputDir, baseName, combine); break;
98
- case "dwg": ExportDwg(doc, views, outputDir, baseName); break;
99
- case "png": ExportPng(doc, views, outputDir, baseName); break;
100
- default: commitWarnings = ExportIfc(doc, views, outputDir, baseName); break;
101
- }
102
- }
103
- catch (Exception ex)
104
- {
105
- // A failed export may already have written some files. A Revit
106
- // transaction rollback cannot undo those filesystem changes.
107
- string observed;
108
- try
109
- {
110
- var changed = FindChangedFiles(outputDir, before).ToList();
111
- observed = changed.Count == 0
112
- ? "No changed files were observed."
113
- : $"Observed {changed.Count} new or changed file(s): {string.Join(", ", changed.Take(20))}{(changed.Count > 20 ? " (additional files omitted)" : string.Empty)}.";
114
- }
115
- catch (Exception scanError)
116
- {
117
- observed = $"Could not inspect remaining files: {scanError.Message}.";
118
- }
119
- throw new InvalidOperationException($"{ex.Message} The export failed; files in '{outputDir}' may be incomplete and were not removed. {observed}", ex);
120
- }
121
-
122
- var files = FindChangedFiles(outputDir, before)
100
+ IReadOnlyList<string> commitWarnings = Array.Empty<string>();
101
+ try
102
+ {
103
+ switch (format)
104
+ {
105
+ case "pdf": ExportPdf(doc, views, outputDir, baseName, combine); break;
106
+ case "dwg": ExportDwg(doc, views, outputDir, baseName); break;
107
+ case "png": ExportPng(doc, views, outputDir, baseName); break;
108
+ default: commitWarnings = ExportIfc(doc, views, outputDir, baseName); break;
109
+ }
110
+ }
111
+ catch (Exception ex)
112
+ {
113
+ // A failed export may already have written some files. A Revit
114
+ // transaction rollback cannot undo those filesystem changes.
115
+ string observed;
116
+ try
117
+ {
118
+ var changed = FindChangedFiles(outputDir, before).ToList();
119
+ observed = changed.Count == 0
120
+ ? "No changed files were observed."
121
+ : $"Observed {changed.Count} new or changed file(s): {string.Join(", ", changed.Take(20))}{(changed.Count > 20 ? " (additional files omitted)" : string.Empty)}.";
122
+ }
123
+ catch (Exception scanError)
124
+ {
125
+ observed = $"Could not inspect remaining files: {scanError.Message}.";
126
+ }
127
+ throw new InvalidOperationException($"{ex.Message} The export failed; files in '{outputDir}' may be incomplete and were not removed. {observed}", ex);
128
+ }
129
+
130
+ var files = FindChangedFiles(outputDir, before)
123
131
  .Select(path => new Dictionary<string, object?>
124
132
  {
125
133
  ["path"] = path,
@@ -130,23 +138,23 @@ namespace RevitBridge.Tools
130
138
  throw new InvalidOperationException($"The {format} export finished but produced no files in {outputDir}.");
131
139
 
132
140
  string sample = string.Join(", ", files.Take(3).Select(file => Path.GetFileName((string)file["path"]!)));
133
- string compact = $"Exported {files.Count} {format.ToUpperInvariant()} file(s) to {outputDir}: {sample}{(files.Count > 3 ? $" (+{files.Count - 3} more)" : string.Empty)}.";
134
- if (commitWarnings.Count > 0)
135
- compact += $" {commitWarnings.Count} Revit warning(s) auto-dismissed (see commitWarnings).";
141
+ string compact = $"Exported {files.Count} {format.ToUpperInvariant()} file(s) to {outputDir}: {sample}{(files.Count > 3 ? $" (+{files.Count - 3} more)" : string.Empty)}.";
142
+ if (commitWarnings.Count > 0)
143
+ compact += $" {commitWarnings.Count} Revit warning(s) auto-dismissed (see commitWarnings).";
136
144
  return new ToolOutput(new
137
145
  {
138
146
  format,
139
147
  outputDir,
140
148
  fileCount = files.Count,
141
- files,
142
- commitWarnings,
143
- }, compact);
144
- }
145
-
146
- private static IEnumerable<string> FindChangedFiles(string directory, IReadOnlyDictionary<string, DateTime> before)
147
- => Directory.GetFiles(directory)
148
- .Where(path => !before.TryGetValue(path, out DateTime writtenBefore) || SafeWriteTime(path) != writtenBefore)
149
- .OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
149
+ files,
150
+ commitWarnings,
151
+ }, compact);
152
+ }
153
+
154
+ private static IEnumerable<string> FindChangedFiles(string directory, IReadOnlyDictionary<string, DateTime> before)
155
+ => Directory.GetFiles(directory)
156
+ .Where(path => !before.TryGetValue(path, out DateTime writtenBefore) || SafeWriteTime(path) != writtenBefore)
157
+ .OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
150
158
 
151
159
  /// <summary>path -> last write time for the files already in the output directory.
152
160
  /// A file whose time cannot be read is left out, so the export reports it if it
@@ -228,29 +236,29 @@ namespace RevitBridge.Tools
228
236
  /// <summary>The IFC exporter writes IFC GUID parameters onto exported elements,
229
237
  /// so the Revit API requires a transaction around it — owned here, matching
230
238
  /// Revit's own behavior of committing those GUIDs on export.</summary>
231
- private static IReadOnlyList<string> ExportIfc(Document doc, List<View> views, string outputDir, string baseName)
239
+ private static IReadOnlyList<string> ExportIfc(Document doc, List<View> views, string outputDir, string baseName)
232
240
  {
233
241
  var options = new IFCExportOptions();
234
242
  if (views.Count == 1)
235
243
  options.FilterViewId = views[0].Id;
236
244
 
237
245
  using var transaction = new Transaction(doc, "export_documents: ifc");
238
- if (transaction.Start() != TransactionStatus.Started)
239
- throw new InvalidOperationException("Unable to start the IFC export transaction.");
240
- var failureGuard = FailureGuard.Attach(transaction);
246
+ if (transaction.Start() != TransactionStatus.Started)
247
+ throw new InvalidOperationException("Unable to start the IFC export transaction.");
248
+ var failureGuard = FailureGuard.Attach(transaction);
241
249
  try
242
250
  {
243
251
  if (!Run(() => doc.Export(outputDir, baseName, options), "IFC"))
244
252
  throw new InvalidOperationException("Revit reported a failed IFC export.");
245
- var status = transaction.Commit();
246
- var finalStatus = transaction.GetStatus();
247
- if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
248
- throw new InvalidOperationException($"The IFC export commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
249
- return failureGuard.Warnings;
250
- }
251
- catch (Exception ex)
252
- {
253
- throw new InvalidOperationException($"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
253
+ var status = transaction.Commit();
254
+ var finalStatus = transaction.GetStatus();
255
+ if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
256
+ throw new InvalidOperationException($"The IFC export commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
257
+ return failureGuard.Warnings;
258
+ }
259
+ catch (Exception ex)
260
+ {
261
+ throw new InvalidOperationException($"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
254
262
  }
255
263
  }
256
264
 
@@ -311,72 +319,72 @@ namespace RevitBridge.Tools
311
319
  }
312
320
 
313
321
  /// <summary>Per-model folder under the pi-revit workspace:
314
- /// Documents\pi-revit\Models\&lt;model title&gt;--&lt;identity hash&gt;.
315
- /// Existing title-only folders are left untouched; identity markers are
316
- /// diagnostic records, not the mechanism that separates exports.</summary>
317
- private static string ModelFolder(Document doc)
322
+ /// Documents\pi-revit\Models\&lt;model title&gt;--&lt;identity hash&gt;.
323
+ /// Existing title-only folders are left untouched; identity markers are
324
+ /// diagnostic records, not the mechanism that separates exports.</summary>
325
+ private static string ModelFolder(Document doc)
318
326
  {
319
327
  string workspace = Path.Combine(
320
328
  Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "pi-revit");
321
- string identity = GetModelIdentity(doc);
322
- string folder = Path.Combine(workspace, "Models", GetModelFolderName(doc.Title, identity));
323
- Directory.CreateDirectory(folder);
324
- TryRecordModelIdentity(folder, doc, identity);
325
- return folder;
326
- }
327
-
328
- internal static string GetModelFolderName(string title, string identity)
329
- {
330
- string cleanTitle = SanitizeFileName(string.IsNullOrWhiteSpace(title) ? "untitled" : title);
331
- // Bound the default path component, leaving room for the identity and exports.
332
- if (cleanTitle.Length > 80)
333
- cleanTitle = cleanTitle[..80];
334
- string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant()[..24];
335
- return $"{cleanTitle}--{hash}";
336
- }
337
-
338
- // Revit can return different managed wrappers for one open native document.
339
- // Document.Equals/GetHashCode identify that open document, unlike reference
340
- // equality in ConditionalWeakTable. Closed document entries are pruned below.
341
- // Tool execution and this dictionary are confined to the Revit API thread.
342
- private static readonly Dictionary<Document, string> OpenDocumentIdentities = new();
343
-
344
- internal static string GetModelIdentity(Document doc)
345
- {
346
- try
347
- {
348
- if (doc.IsModelInCloud)
349
- {
350
- var path = doc.GetCloudModelPath();
351
- return $"cloud:{path.Region.ToUpperInvariant()}:{path.GetProjectGUID():D}:{path.GetModelGUID():D}";
352
- }
353
- string pathName = doc.PathName;
354
- if (!string.IsNullOrWhiteSpace(pathName))
355
- {
356
- // Revit file paths are Windows paths; canonicalize separators,
357
- // relative segments and case. Revit Server identities use RSN URLs.
358
- if (pathName.StartsWith("RSN://", StringComparison.OrdinalIgnoreCase))
359
- return "server:" + pathName.Replace('\\', '/').TrimEnd('/').ToUpperInvariant();
360
- return "file:" + Path.GetFullPath(pathName).Replace('/', '\\').ToUpperInvariant();
361
- }
362
- }
363
- catch
364
- {
365
- // An unavailable identity must not collapse unrelated documents into
366
- // a shared title-only directory. Use the same safe fallback as unsaved docs.
367
- }
368
- foreach (var closed in OpenDocumentIdentities.Keys.Where(key => !key.IsValidObject).ToList())
369
- OpenDocumentIdentities.Remove(closed);
370
- if (!OpenDocumentIdentities.TryGetValue(doc, out string? identity))
371
- OpenDocumentIdentities[doc] = identity = $"session:{Guid.NewGuid():N}";
372
- return identity;
373
- }
374
-
375
- private static void TryRecordModelIdentity(string folder, Document doc, string identity)
329
+ string identity = GetModelIdentity(doc);
330
+ string folder = Path.Combine(workspace, "Models", GetModelFolderName(doc.Title, identity));
331
+ Directory.CreateDirectory(folder);
332
+ TryRecordModelIdentity(folder, doc, identity);
333
+ return folder;
334
+ }
335
+
336
+ internal static string GetModelFolderName(string title, string identity)
337
+ {
338
+ string cleanTitle = SanitizeFileName(string.IsNullOrWhiteSpace(title) ? "untitled" : title);
339
+ // Bound the default path component, leaving room for the identity and exports.
340
+ if (cleanTitle.Length > 80)
341
+ cleanTitle = cleanTitle[..80];
342
+ string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant()[..24];
343
+ return $"{cleanTitle}--{hash}";
344
+ }
345
+
346
+ // Revit can return different managed wrappers for one open native document.
347
+ // Document.Equals/GetHashCode identify that open document, unlike reference
348
+ // equality in ConditionalWeakTable. Closed document entries are pruned below.
349
+ // Tool execution and this dictionary are confined to the Revit API thread.
350
+ private static readonly Dictionary<Document, string> OpenDocumentIdentities = new();
351
+
352
+ internal static string GetModelIdentity(Document doc)
353
+ {
354
+ try
355
+ {
356
+ if (doc.IsModelInCloud)
357
+ {
358
+ var path = doc.GetCloudModelPath();
359
+ return $"cloud:{path.Region.ToUpperInvariant()}:{path.GetProjectGUID():D}:{path.GetModelGUID():D}";
360
+ }
361
+ string pathName = doc.PathName;
362
+ if (!string.IsNullOrWhiteSpace(pathName))
363
+ {
364
+ // Revit file paths are Windows paths; canonicalize separators,
365
+ // relative segments and case. Revit Server identities use RSN URLs.
366
+ if (pathName.StartsWith("RSN://", StringComparison.OrdinalIgnoreCase))
367
+ return "server:" + pathName.Replace('\\', '/').TrimEnd('/').ToUpperInvariant();
368
+ return "file:" + Path.GetFullPath(pathName).Replace('/', '\\').ToUpperInvariant();
369
+ }
370
+ }
371
+ catch
372
+ {
373
+ // An unavailable identity must not collapse unrelated documents into
374
+ // a shared title-only directory. Use the same safe fallback as unsaved docs.
375
+ }
376
+ foreach (var closed in OpenDocumentIdentities.Keys.Where(key => !key.IsValidObject).ToList())
377
+ OpenDocumentIdentities.Remove(closed);
378
+ if (!OpenDocumentIdentities.TryGetValue(doc, out string? identity))
379
+ OpenDocumentIdentities[doc] = identity = $"session:{Guid.NewGuid():N}";
380
+ return identity;
381
+ }
382
+
383
+ private static void TryRecordModelIdentity(string folder, Document doc, string identity)
376
384
  {
377
385
  try
378
386
  {
379
- string line = $"{identity}\t{doc.PathName}";
387
+ string line = $"{identity}\t{doc.PathName}";
380
388
  string marker = Path.Combine(folder, "model.txt");
381
389
  if (!File.Exists(marker) || !File.ReadAllLines(marker).Contains(line))
382
390
  File.AppendAllLines(marker, new[] { line });
@@ -9,6 +9,14 @@ namespace RevitBridge.Tools
9
9
  private const int MaxIds = 50;
10
10
 
11
11
  public string Name => "get_element_details";
12
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
13
+ public IReadOnlyList<string> Keywords => new[] { "parameters", "properties", "inspect", "values", "material", "bounding box", "location" };
14
+ public IReadOnlyList<ToolLimit> Limits => new[]
15
+ {
16
+ new ToolLimit("Listing or filtering elements", "tool", "get_elements"),
17
+ new ToolLimit("Elements inside linked models", "tool", "get_linked_elements"),
18
+ new ToolLimit("Exact solid geometry or faces", "api", "Element.Geometry(Options)"),
19
+ };
12
20
  public string Label => "Get Element Details";
13
21
  public string Description => "Inspect one or more elements by id: parameter VALUES (name, internal value, formatted displayValue, storage type, display unit where applicable, read-only/shared flags), plus optional type parameters, location (point/curve), bounding box, and materials. Internal numeric values are Revit internal units (feet-based); displayValue is formatted in the document's display units and 'unit' names that display unit. Use get_elements parameter_names to project a small set of values alongside a query.";
14
22
 
@@ -36,7 +44,7 @@ namespace RevitBridge.Tools
36
44
  properties = new
37
45
  {
38
46
  parameters = new { type = "boolean", description = "Instance parameter values. Default true." },
39
- type_parameters = new { type = "boolean", description = "Include the element type's parameters, marked isType=true, independently of the instance parameters flag. Default false." },
47
+ type_parameters = new { type = "boolean", description = "Include the element type's parameters, marked isType=true, independently of the instance parameters flag. Default false." },
40
48
  location = new { type = "boolean", description = "Location point or curve (coordinates in internal feet). Default false." },
41
49
  bounding_box = new { type = "boolean", description = "Model bounding box min/max (internal feet). Default false." },
42
50
  materials = new { type = "boolean", description = "Material ids/names with area/volume (internal units). Default false." },
@@ -70,9 +78,9 @@ namespace RevitBridge.Tools
70
78
  JsonElement include = args.TryGetProperty("include", out var includeElement) && includeElement.ValueKind == JsonValueKind.Object
71
79
  ? includeElement
72
80
  : default;
73
- bool withParameters = JsonArgs.GetBool(include, "parameters", true);
74
- bool withTypeParameters = JsonArgs.GetBool(include, "type_parameters", false);
75
- bool withAnyParameters = withParameters || withTypeParameters;
81
+ bool withParameters = JsonArgs.GetBool(include, "parameters", true);
82
+ bool withTypeParameters = JsonArgs.GetBool(include, "type_parameters", false);
83
+ bool withAnyParameters = withParameters || withTypeParameters;
76
84
  bool withLocation = JsonArgs.GetBool(include, "location", false);
77
85
  bool withBoundingBox = JsonArgs.GetBool(include, "bounding_box", false);
78
86
  bool withMaterials = JsonArgs.GetBool(include, "materials", false);
@@ -103,11 +111,11 @@ namespace RevitBridge.Tools
103
111
 
104
112
  int parameterCount = 0;
105
113
  int parameterTotal = 0;
106
- if (withAnyParameters)
107
- {
108
- var parameters = new List<Dictionary<string, object?>>();
109
- if (withParameters)
110
- parameterTotal += AppendParameters(doc, element, nameFilter, isType: false, parameters);
114
+ if (withAnyParameters)
115
+ {
116
+ var parameters = new List<Dictionary<string, object?>>();
117
+ if (withParameters)
118
+ parameterTotal += AppendParameters(doc, element, nameFilter, isType: false, parameters);
111
119
  if (withTypeParameters && elementType != null)
112
120
  parameterTotal += AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
113
121
  dto["parameters"] = parameters;
@@ -124,7 +132,7 @@ namespace RevitBridge.Tools
124
132
  elements.Add(dto);
125
133
  // With a name filter active, "0 params" is ambiguous (none matched vs none
126
134
  // exist): report matched-of-total so a localization miss is visible.
127
- string paramSummary = !withAnyParameters ? string.Empty
135
+ string paramSummary = !withAnyParameters ? string.Empty
128
136
  : nameFilter != null ? $", {parameterCount} of {parameterTotal} params matched parameter_names"
129
137
  : $", {parameterCount} params";
130
138
  compactParts.Add($"'{element.Name}' (id {id}, {element.Category?.Name ?? "no category"}{paramSummary})");
@@ -167,37 +175,25 @@ namespace RevitBridge.Tools
167
175
  return total;
168
176
  }
169
177
 
170
- internal static IReadOnlyList<object> ProjectParameters(Document doc, Element element, IReadOnlyList<string> names, bool includeType)
171
- {
172
- var result = new List<object>();
173
- void Project(Element source, bool isType)
174
- {
175
- foreach (string input in names.Distinct(StringComparer.OrdinalIgnoreCase))
176
- {
177
- IEnumerable<Parameter> matches;
178
- if (input.StartsWith("guid:", StringComparison.OrdinalIgnoreCase))
179
- {
180
- if (!Guid.TryParse(input[5..], out var guid)) throw new ArgumentException($"Invalid shared parameter identity: {input}");
181
- var parameter = source.get_Parameter(guid);
182
- matches = parameter == null ? Array.Empty<Parameter>() : new[] { parameter };
183
- }
184
- else if (Enum.TryParse<BuiltInParameter>(input, true, out var builtIn) && Enum.IsDefined(builtIn) && builtIn != BuiltInParameter.INVALID)
185
- {
186
- var parameter = source.get_Parameter(builtIn);
187
- matches = parameter == null ? Array.Empty<Parameter>() : new[] { parameter };
188
- }
189
- else
190
- matches = source.Parameters.Cast<Parameter>().Where(p => string.Equals(p.Definition?.Name, input, StringComparison.OrdinalIgnoreCase));
191
- var values = matches.Select(p => DescribeParameter(doc, p, p.Definition?.Name ?? "", isType)).ToArray();
192
- result.Add(new { requested = input, isType, found = values.Length > 0, ambiguous = values.Length > 1, matches = values });
193
- }
194
- }
195
- Project(element, false);
196
- if (includeType && doc.GetElement(element.GetTypeId()) is { } type) Project(type, true);
197
- return result;
198
- }
199
-
200
- internal static Dictionary<string, object?> DescribeParameter(Document doc, Parameter parameter, string name, bool isType)
178
+ internal static IReadOnlyList<object> ProjectParameters(Document doc, Element element, IReadOnlyList<string> names, bool includeType)
179
+ {
180
+ var result = new List<object>();
181
+ void Project(Element source, bool isType)
182
+ {
183
+ foreach (string input in names.Distinct(StringComparer.OrdinalIgnoreCase))
184
+ {
185
+ // Shared policy: projections report every match and flag ambiguity instead of choosing.
186
+ var matches = ParameterResolver.FindAll(source, ParameterReference.Parse(input));
187
+ var values = matches.Select(p => DescribeParameter(doc, p, p.Definition?.Name ?? "", isType)).ToArray();
188
+ result.Add(new { requested = input, isType, found = values.Length > 0, ambiguous = values.Length > 1, matches = values });
189
+ }
190
+ }
191
+ Project(element, false);
192
+ if (includeType && doc.GetElement(element.GetTypeId()) is { } type) Project(type, true);
193
+ return result;
194
+ }
195
+
196
+ internal static Dictionary<string, object?> DescribeParameter(Document doc, Parameter parameter, string name, bool isType)
201
197
  {
202
198
  bool hasValue = parameter.HasValue;
203
199
  var row = new Dictionary<string, object?>
@@ -1,76 +1,82 @@
1
- using System.Text.Json;
2
- using Autodesk.Revit.DB;
3
-
4
- namespace RevitBridge.Tools;
5
-
6
- internal sealed class GetElementRelationships : ITool
7
- {
8
- public string Name => "get_element_relationships";
9
- public string Label => "Get Element Relationships";
10
- public string Tier => "advanced";
11
- public string Description => "Inspect host-document element relationships: type, level, owning view, host, family parent/subcomponents, group or assembly membership, joined geometry and logical dependents. Dependents are not a complete deletion-impact prediction. Pagination applies independently to each relationship; links are not traversed.";
12
- private static readonly string[] Kinds = { "type", "level", "owner_view", "host", "parent", "subcomponents", "group", "assembly", "members", "joined", "dependents" };
13
- public object ParametersSchema => new
14
- {
15
- type = "object",
16
- properties = new
17
- {
18
- element_id = new { type = "integer" },
19
- relationships = new { type = "array", items = new { type = "string", @enum = Kinds } },
20
- offset = new { type = "integer", minimum = 0 },
21
- limit = new { type = "integer", minimum = 1, maximum = 200 },
22
- },
23
- required = new[] { "element_id" },
24
- };
25
-
26
- public object Execute(JsonElement args, ToolContext context)
27
- {
28
- var doc = context.Document ?? throw new NoActiveDocumentException();
29
- var id = JsonArgs.GetLong(args, "element_id") ?? throw new ArgumentException("element_id is required.");
30
- var element = doc.GetElement(new ElementId(id)) ?? throw new ArgumentException("element_id does not exist in the active document.");
31
- var kinds = (IReadOnlyList<string>?)JsonArgs.GetStringArray(args, "relationships") ?? Kinds;
32
- if (kinds.Any(kind => !Kinds.Contains(kind))) throw new ArgumentException("Unknown relationship kind.");
33
- int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
34
- int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 200);
35
- var result = new Dictionary<string, object?>();
36
- foreach (var kind in kinds.Distinct())
37
- {
38
- IEnumerable<ElementId> ids = kind switch
39
- {
40
- "type" => One(element.GetTypeId()),
41
- "level" => One(element.LevelId),
42
- "owner_view" => One(element.OwnerViewId),
43
- "host" => One((element as FamilyInstance)?.Host?.Id),
44
- "parent" => One((element as FamilyInstance)?.SuperComponent?.Id),
45
- "subcomponents" => element is FamilyInstance family ? family.GetSubComponentIds() : Array.Empty<ElementId>(),
46
- "group" => One(element.GroupId),
47
- "assembly" => One(element.AssemblyInstanceId),
48
- "members" => element is Group group ? group.GetMemberIds()
49
- : element is AssemblyInstance assembly ? assembly.GetMemberIds() : Array.Empty<ElementId>(),
50
- "joined" => Joined(doc, element),
51
- "dependents" => element.GetDependentElements(null),
52
- _ => Array.Empty<ElementId>(),
53
- };
54
- var all = ids.Where(x => x != ElementId.InvalidElementId).DistinctBy(x => x.Value).OrderBy(x => x.Value).ToList();
55
- var rows = all.Skip(offset).Take(limit).Select(x =>
56
- {
57
- var related = doc.GetElement(x);
58
- return new { id = x.Value, name = related?.Name, category = related?.Category?.Name, unique_id = related?.UniqueId };
59
- }).ToArray();
60
- result[kind] = new { total_count = all.Count, returned_count = rows.Length, offset,
61
- has_more = offset + rows.Length < all.Count,
62
- next_offset = offset + rows.Length < all.Count ? (int?)(offset + rows.Length) : null, elements = rows };
63
- }
64
- return new { document_id = DocumentGuard.GetIdentity(doc), element_id = id, relationships = result };
65
- }
66
-
67
- private static IEnumerable<ElementId> One(ElementId? id)
68
- => id == null || id == ElementId.InvalidElementId ? Array.Empty<ElementId>() : new[] { id };
69
-
70
- private static IEnumerable<ElementId> Joined(Document doc, Element element)
71
- {
72
- // JoinGeometryUtils rejects family documents; surface that restriction explicitly.
73
- if (doc.IsFamilyDocument) throw new ArgumentException("joined relationships require a project document. Request other relationship kinds for families.");
74
- return JoinGeometryUtils.GetJoinedElements(doc, element);
75
- }
76
- }
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class GetElementRelationships : ITool
7
+ {
8
+ public string Name => "get_element_relationships";
9
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
10
+ public IReadOnlyList<string> Keywords => new[] { "host", "hosted", "dependents", "nested", "group members", "joined", "relationship", "family hierarchy" };
11
+ public IReadOnlyList<ToolLimit> Limits => new[]
12
+ {
13
+ new ToolLimit("A complete prediction of deletion effects", "tool", "delete_elements with preview"),
14
+ };
15
+ public string Label => "Get Element Relationships";
16
+ public string Tier => "advanced";
17
+ public string Description => "Inspect host-document element relationships: type, level, owning view, host, family parent/subcomponents, group or assembly membership, joined geometry and logical dependents. Dependents are not a complete deletion-impact prediction. Pagination applies independently to each relationship; links are not traversed.";
18
+ private static readonly string[] Kinds = { "type", "level", "owner_view", "host", "parent", "subcomponents", "group", "assembly", "members", "joined", "dependents" };
19
+ public object ParametersSchema => new
20
+ {
21
+ type = "object",
22
+ properties = new
23
+ {
24
+ element_id = new { type = "integer" },
25
+ relationships = new { type = "array", items = new { type = "string", @enum = Kinds } },
26
+ offset = new { type = "integer", minimum = 0 },
27
+ limit = new { type = "integer", minimum = 1, maximum = 200 },
28
+ },
29
+ required = new[] { "element_id" },
30
+ };
31
+
32
+ public object Execute(JsonElement args, ToolContext context)
33
+ {
34
+ var doc = context.Document ?? throw new NoActiveDocumentException();
35
+ var id = JsonArgs.GetLong(args, "element_id") ?? throw new ArgumentException("element_id is required.");
36
+ var element = doc.GetElement(new ElementId(id)) ?? throw new ArgumentException("element_id does not exist in the active document.");
37
+ var kinds = (IReadOnlyList<string>?)JsonArgs.GetStringArray(args, "relationships") ?? Kinds;
38
+ if (kinds.Any(kind => !Kinds.Contains(kind))) throw new ArgumentException("Unknown relationship kind.");
39
+ int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
40
+ int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 200);
41
+ var result = new Dictionary<string, object?>();
42
+ foreach (var kind in kinds.Distinct())
43
+ {
44
+ IEnumerable<ElementId> ids = kind switch
45
+ {
46
+ "type" => One(element.GetTypeId()),
47
+ "level" => One(element.LevelId),
48
+ "owner_view" => One(element.OwnerViewId),
49
+ "host" => One((element as FamilyInstance)?.Host?.Id),
50
+ "parent" => One((element as FamilyInstance)?.SuperComponent?.Id),
51
+ "subcomponents" => element is FamilyInstance family ? family.GetSubComponentIds() : Array.Empty<ElementId>(),
52
+ "group" => One(element.GroupId),
53
+ "assembly" => One(element.AssemblyInstanceId),
54
+ "members" => element is Group group ? group.GetMemberIds()
55
+ : element is AssemblyInstance assembly ? assembly.GetMemberIds() : Array.Empty<ElementId>(),
56
+ "joined" => Joined(doc, element),
57
+ "dependents" => element.GetDependentElements(null),
58
+ _ => Array.Empty<ElementId>(),
59
+ };
60
+ var all = ids.Where(x => x != ElementId.InvalidElementId).DistinctBy(x => x.Value).OrderBy(x => x.Value).ToList();
61
+ var rows = all.Skip(offset).Take(limit).Select(x =>
62
+ {
63
+ var related = doc.GetElement(x);
64
+ return new { id = x.Value, name = related?.Name, category = related?.Category?.Name, unique_id = related?.UniqueId };
65
+ }).ToArray();
66
+ result[kind] = new { total_count = all.Count, returned_count = rows.Length, offset,
67
+ has_more = offset + rows.Length < all.Count,
68
+ next_offset = offset + rows.Length < all.Count ? (int?)(offset + rows.Length) : null, elements = rows };
69
+ }
70
+ return new { document_id = DocumentGuard.GetIdentity(doc), element_id = id, relationships = result };
71
+ }
72
+
73
+ private static IEnumerable<ElementId> One(ElementId? id)
74
+ => id == null || id == ElementId.InvalidElementId ? Array.Empty<ElementId>() : new[] { id };
75
+
76
+ private static IEnumerable<ElementId> Joined(Document doc, Element element)
77
+ {
78
+ // JoinGeometryUtils rejects family documents; surface that restriction explicitly.
79
+ if (doc.IsFamilyDocument) throw new ArgumentException("joined relationships require a project document. Request other relationship kinds for families.");
80
+ return JoinGeometryUtils.GetJoinedElements(doc, element);
81
+ }
82
+ }
@@ -9,6 +9,14 @@ namespace RevitBridge.Tools
9
9
  private const int MaxLimit = 1000;
10
10
 
11
11
  public string Name => "get_element_types";
12
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
13
+ public IReadOnlyList<string> Keywords => new[] { "types", "family types", "family symbols", "unused types", "loaded families", "purge candidates" };
14
+ public IReadOnlyList<ToolLimit> Limits => new[]
15
+ {
16
+ new ToolLimit("Creating or duplicating types", "api", "ElementType.Duplicate"),
17
+ new ToolLimit("Loading families", "api", "Document.LoadFamily"),
18
+ new ToolLimit("The types of the family being edited in a family document", "api", DocumentKind.FamilyApi),
19
+ };
12
20
  public string Label => "Get Element Types";
13
21
  public string Description => "List element types / family symbols (wall types, door types, ...) for a category or class: id, name, familyName, category, isFamilySymbol, optional placed-instance count per type (answers 'used vs merely loaded'). Use the ids with get_elements type_id or for type assignment.";
14
22