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
@@ -33,8 +33,15 @@ namespace RevitBridge.Tools
33
33
  private const int MaxExceptionsPerMember = 8;
34
34
 
35
35
  public string Name => "search_api_docs";
36
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
37
+ public IReadOnlyList<string> Keywords => new[] { "api", "revit api", "method", "class", "signature", "enum", "developer documentation" };
38
+ public IReadOnlyList<ToolLimit> Limits => new[]
39
+ {
40
+ new ToolLimit("Autodesk product Help or modeling guidance", "user", "Autodesk Revit Help or project standards"),
41
+ new ToolLimit("Proof that code compiles or works", "tool", "execute_csharp"),
42
+ };
36
43
  public string Label => "Search API Docs";
37
- public string Description => "Search the offline Revit API documentation (the RevitAPI.xml and RevitAPIUI.xml files shipped with Revit) for types, methods, constructors, properties, fields, and events; every public API enum is fully searchable by value name (values the XML leaves undocumented are synthesized from the API assemblies). query is a single name or substring — e.g. 'FilteredElementCollector', 'Wall.Create', 'WALL_BASE_OFFSET' — ranked: exact name first, then prefix, then substring; dotted Type.Member queries match composites, and same-named overloads rank simplest-first. To target one overload, continue the query past a parenthesis with parameter types, e.g. 'Wall.Create(Document, Curve'. Returns signatures with summary, remarks, parameter docs, return docs, exception docs, and the Revit version a member was introduced in ('since'); the top match shows its full docs inline. Works with no document open. Use it to verify exact classes, members, and signatures before writing execute_csharp code. The first query builds the index (a few seconds); later queries are instant.";
44
+ public string Description => "Search the offline Revit API documentation (the RevitAPI.xml and RevitAPIUI.xml files shipped with Revit) for types, methods, constructors, properties, fields, and events; every public API enum is fully searchable by value name (values the XML leaves undocumented are synthesized from the API assemblies). query is a name or substring — e.g. 'FilteredElementCollector', 'Wall.Create', 'WALL_BASE_OFFSET' — ranked: exact name first, then prefix, then substring; dotted Type.Member queries match composites, and same-named overloads rank simplest-first. To target one overload, continue the query past a parenthesis with parameter types, e.g. 'Wall.Create(Document, Curve'. Returns signatures with summary, remarks, parameter docs, return docs, exception docs, and the Revit version a member was introduced in ('since'); the top match shows its full docs inline. To verify several members at once, separate them with ';' (up to 10, e.g. 'View3D.CreatePerspective; ViewOrientation3D', the form declared API limits use): each gets its best match with compact docs. Works with no document open. Use it to verify exact classes, members, and signatures before writing execute_csharp code. The first query builds the index (a few seconds); later queries are instant.";
38
45
 
39
46
  public bool RequiresDocument => false;
40
47
 
@@ -46,7 +53,7 @@ namespace RevitBridge.Tools
46
53
  query = new
47
54
  {
48
55
  type = "string",
49
- description = "Type or member name to look up, e.g. FilteredElementCollector, Wall.Create, WALL_BASE_OFFSET. Substring and multi-word matches are ranked; Type.Member composites rank highest on exact match.",
56
+ description = "Type or member name to look up, e.g. FilteredElementCollector, Wall.Create, WALL_BASE_OFFSET; or up to 10 names separated by ';' to verify them in one call. Substring and multi-word matches are ranked; Type.Member composites rank highest on exact match.",
50
57
  },
51
58
  kind = new
52
59
  {
@@ -57,7 +64,7 @@ namespace RevitBridge.Tools
57
64
  max_results = new
58
65
  {
59
66
  type = "integer",
60
- description = $"Maximum matches to return (1-{MaxResultsCap}, default {DefaultMaxResults}).",
67
+ description = $"Maximum matches to return (1-{MaxResultsCap}, default {DefaultMaxResults}; per member when several are named, default {DefaultResultsPerMember}).",
61
68
  },
62
69
  },
63
70
  required = new[] { "query" },
@@ -66,7 +73,7 @@ namespace RevitBridge.Tools
66
73
  public string? PromptSnippet => "Search the offline Revit API documentation for types, members, and signatures.";
67
74
  public IReadOnlyList<string>? PromptGuidelines => new[]
68
75
  {
69
- "Before writing execute_csharp code, verify exact classes and member signatures with search_api_docs (e.g. query 'Wall.Create' or 'FilteredElementCollector').",
76
+ "Before writing execute_csharp code, verify exact classes and member signatures with search_api_docs; name all members you need in one query separated by ';' (e.g. 'View3D.CreatePerspective; ViewOrientation3D; View.CropBox').",
70
77
  };
71
78
 
72
79
  public object? Execute(JsonElement args, ToolContext context)
@@ -81,6 +88,12 @@ namespace RevitBridge.Tools
81
88
  if (index.Members.Count == 0)
82
89
  throw new InvalidOperationException("The Revit API documentation index is empty. " + string.Join(" ", index.Warnings));
83
90
 
91
+ var parts = SplitQueries(query);
92
+ if (parts.Count > MaxQueriesPerCall)
93
+ throw new ArgumentException($"query names {parts.Count} members; look up at most {MaxQueriesPerCall} per call.");
94
+ if (parts.Count > 1)
95
+ return ExecuteMany(index, parts, kindFilter, args.TryGetProperty("max_results", out _) ? maxResults : DefaultResultsPerMember);
96
+
84
97
  var (top, total, rewriteNote) = Search(index, query, kindFilter, maxResults);
85
98
 
86
99
  var matches = new List<Dictionary<string, object?>>(top.Count);
@@ -119,10 +132,65 @@ namespace RevitBridge.Tools
119
132
  }, BuildMarkdown(query, top, total, index) + (rewriteNote is null ? string.Empty : $"\nNote: {rewriteNote}"));
120
133
  }
121
134
 
135
+ private const int DefaultResultsPerMember = 3;
136
+ private const int ManyRemarksChars = 300;
137
+ private const int ManyParamDocChars = 160;
138
+
139
+ /// <summary>Several members in one call: the best match of each with compact docs, plus
140
+ /// the next candidates by signature only, so a script's members are verified together.</summary>
141
+ private static ToolOutput ExecuteMany(DocIndex index, IReadOnlyList<string> parts, char? kindFilter, int perQuery)
142
+ {
143
+ var lookups = LookUpMany(index, parts, kindFilter, perQuery);
144
+ var results = lookups.Select(lookup => new Dictionary<string, object?>
145
+ {
146
+ ["query"] = lookup.Query,
147
+ ["totalMatches"] = lookup.Total,
148
+ ["best"] = lookup.Top.Count == 0 ? null : new Dictionary<string, object?>
149
+ {
150
+ ["kind"] = KindLabel(lookup.Top[0]), ["name"] = lookup.Top[0].FullName, ["signature"] = lookup.Top[0].Signature, ["since"] = lookup.Top[0].Since,
151
+ ["summary"] = lookup.Top[0].Summary, ["remarks"] = Cap(lookup.Top[0].Remarks, ManyRemarksChars),
152
+ ["parameters"] = lookup.Top[0].Parameters?.Select(pair => new Dictionary<string, object?> { ["name"] = pair.Key, ["description"] = Cap(pair.Value, ManyParamDocChars) }).ToList(),
153
+ ["returns"] = Cap(lookup.Top[0].Returns, ManyParamDocChars),
154
+ // Exceptions state when a member refuses (for example "not a project document").
155
+ ["exceptions"] = lookup.Top[0].Exceptions?.Take(3).Select(pair => $"{pair.Key}: {Cap(FirstSentence(pair.Value), 140)}").ToList(),
156
+ },
157
+ ["alternatives"] = lookup.Top.Skip(1).Select(member => new Dictionary<string, object?> { ["kind"] = KindLabel(member), ["signature"] = member.Signature }).ToList(),
158
+ ["note"] = lookup.Total == 0 ? "No match. Try a shorter name or the type alone." : lookup.Note,
159
+ }).ToList();
160
+ var markdown = new StringBuilder($"Revit API docs for {parts.Count} members:");
161
+ foreach (var lookup in lookups)
162
+ markdown.Append(lookup.Top.Count == 0 ? $"\n- '{lookup.Query}': no match" : $"\n- '{lookup.Query}': **{lookup.Top[0].Signature}** — {FirstSentence(lookup.Top[0].Summary) ?? "(no summary)"}");
163
+ return new ToolOutput(new
164
+ {
165
+ query = string.Join("; ", parts),
166
+ lookups = parts.Count,
167
+ results,
168
+ indexedMembers = index.Members.Count,
169
+ sources = index.SourceFiles,
170
+ warnings = index.Warnings.Count > 0 ? index.Warnings : null,
171
+ }, markdown.ToString());
172
+ }
173
+
122
174
  // ----------------------------------------------------------------- search
123
175
 
124
176
  private static readonly char[] WordSeparators = { '.', ' ', '_', '(', ')', ',', ':', '-', '/' };
125
177
 
178
+ /// <summary>Most members one multi-member lookup ("A; B; C") may name.</summary>
179
+ private const int MaxQueriesPerCall = 10;
180
+
181
+ /// <summary>A query naming several members separated by ';' (the form declared API
182
+ /// limits use) is looked up member by member in one call; duplicates are dropped.</summary>
183
+ private static List<string> SplitQueries(string query) => query
184
+ .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
185
+ .Distinct(StringComparer.OrdinalIgnoreCase).ToList();
186
+
187
+ private static List<(string Query, List<ApiMember> Top, int Total, string? Note)> LookUpMany(DocIndex index, IReadOnlyList<string> queries, char? kindFilter, int perQuery)
188
+ => queries.Select(part =>
189
+ {
190
+ var (top, total, note) = Search(index, part, kindFilter, perQuery);
191
+ return (part, top, total, note);
192
+ }).ToList();
193
+
126
194
  private static (List<ApiMember> Top, int Total, string? RewriteNote) Search(DocIndex index, string query, char? kindFilter, int maxResults)
127
195
  {
128
196
  // Signatures are rendered exactly one way ("Name(Type, Type)"): normalize the
@@ -18,6 +18,16 @@ namespace RevitBridge.Tools
18
18
  private const int MaxUpdates = 200;
19
19
 
20
20
  public string Name => "set_parameters";
21
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
22
+ public IReadOnlyList<string> Keywords => new[] { "parameter", "set value", "edit", "change value", "rename", "mark", "comments", "bulk edit", "update" };
23
+ public IReadOnlyList<ToolLimit> Limits => new[]
24
+ {
25
+ new ToolLimit("Changing an element's type", "tool", "change_element_types"),
26
+ new ToolLimit("Element properties that are not parameters, such as pinned state", "api", "Element.Pinned"),
27
+ new ToolLimit("Moving or rotating elements", "tool", "transform_elements"),
28
+ new ToolLimit("Family parameters, family types and formulas in a family document", "api", DocumentKind.FamilyApi),
29
+ };
30
+ public string? Verification => "reread";
21
31
  public string Label => "Set Parameters";
22
32
  public string Description => "Write parameter values on elements — also the home for rename: parameter 'Name' covers levels, views, sheets, types, etc. (falls back to the element's Name property when the Name parameter is read-only). Each update has its own subtransaction inside one model transaction. Default partial success commits valid updates; atomic=true rolls everything back if any update fails. preview=true validates commit inside a transaction group, then rolls the group back. Results include observed per-step before/after values and distinguish succeeded from proposed updates. parameter accepts a display name (Comments, Mark, Name), a BuiltInParameter enum name (e.g. ALL_MODEL_MARK), or guid:<GUID> for a shared parameter; type parameters live on the element type, so pass the type's id. Values are validated against the parameter's storage type; numeric values are interpreted in the document's display units for that parameter unless 'unit' (e.g. millimeters, feet, squareMeters) is given. Revit warnings raised at commit (e.g. duplicate Mark values) are auto-dismissed and listed in commitWarnings — mention them to the user; error-severity failures roll the whole transaction back.";
23
33
  public bool Write => true;
@@ -25,10 +35,10 @@ namespace RevitBridge.Tools
25
35
  public object ParametersSchema => new
26
36
  {
27
37
  type = "object",
28
- properties = new
29
- {
30
- preview = new { type = "boolean", description = "Validate edits through Revit commit, then roll back the enclosing transaction group. Returns proposed before/after values; no model changes remain. Default false." },
31
- atomic = new { type = "boolean", description = "Roll back all updates if any fails. Default false preserves successful updates. Each update is isolated in a subtransaction." },
38
+ properties = new
39
+ {
40
+ preview = new { type = "boolean", description = "Validate edits through Revit commit, then roll back the enclosing transaction group. Returns proposed before/after values; no model changes remain. Default false." },
41
+ atomic = new { type = "boolean", description = "Roll back all updates if any fails. Default false preserves successful updates. Each update is isolated in a subtransaction." },
32
42
  updates = new
33
43
  {
34
44
  type = "array",
@@ -62,37 +72,37 @@ namespace RevitBridge.Tools
62
72
  "set_parameters defaults to partial success; use atomic=true for all-or-nothing batches and preview=true for commit-validated model rollback. Always inspect committed, proposed and failed.",
63
73
  };
64
74
 
65
- public object? Execute(JsonElement args, ToolContext context)
66
- {
67
- var doc = context.Document ?? throw new NoActiveDocumentException();
68
- DocumentGuard.CheckExpectedDocument(args, doc);
69
- var updates = ParseUpdates(args);
70
- var steps = updates.Select(update => new ModelEditBatch.Step(
71
- new Dictionary<string, object?> { ["id"] = update.ElementId, ["parameter"] = update.ParameterInput },
72
- () =>
73
- {
74
- var change = Apply(doc, update);
75
- return new Dictionary<string, object?>
76
- {
77
- ["before"] = change.Before, ["after"] = change.After,
78
- ["newDisplayValue"] = change.Display,
79
- };
80
- })).ToArray();
81
- var result = ModelEditBatch.Run(doc, Name, args, steps);
82
- string summary = result.Preview
83
- ? $"Preview rolled back: {result.Proposed.Count} proposed update(s), {result.Failed.Count} failure(s)."
84
- : result.Committed
85
- ? $"Applied {result.Succeeded.Count} parameter update(s); {result.Failed.Count} failed."
86
- : $"No updates applied; {result.Failed.Count} failed. Batch rolled back.";
87
- return new ToolOutput(result.Payload, summary);
88
- }
89
-
75
+ public object? Execute(JsonElement args, ToolContext context)
76
+ {
77
+ var doc = context.Document ?? throw new NoActiveDocumentException();
78
+ DocumentGuard.CheckExpectedDocument(args, doc);
79
+ var updates = ParseUpdates(args);
80
+ var steps = updates.Select(update => new ModelEditBatch.Step(
81
+ new Dictionary<string, object?> { ["id"] = update.ElementId, ["parameter"] = update.ParameterInput },
82
+ () =>
83
+ {
84
+ var change = Apply(doc, update);
85
+ return new Dictionary<string, object?>
86
+ {
87
+ ["before"] = change.Before, ["after"] = change.After,
88
+ ["newDisplayValue"] = change.Display,
89
+ };
90
+ })).ToArray();
91
+ var result = ModelEditBatch.Run(doc, Name, args, steps);
92
+ string summary = result.Preview
93
+ ? $"Preview rolled back: {result.Proposed.Count} proposed update(s), {result.Failed.Count} failure(s)."
94
+ : result.Committed
95
+ ? $"Applied {result.Succeeded.Count} parameter update(s); {result.Failed.Count} failed."
96
+ : $"No updates applied; {result.Failed.Count} failed. Batch rolled back.";
97
+ return new ToolOutput(result.Payload, summary);
98
+ }
99
+
90
100
  // ------------------------------------------------------------- application
91
101
 
92
- private sealed record ValueChange(string? Display, object? Before, object? After);
93
-
94
- /// <summary>Snapshots the concrete parameter or rename target actually written by this step.</summary>
95
- private static ValueChange Apply(Document doc, Update update)
102
+ private sealed record ValueChange(string? Display, object? Before, object? After);
103
+
104
+ /// <summary>Snapshots the concrete parameter or rename target actually written by this step.</summary>
105
+ private static ValueChange Apply(Document doc, Update update)
96
106
  {
97
107
  var element = doc.GetElement(new ElementId(update.ElementId))
98
108
  ?? throw new InvalidOperationException($"Element {update.ElementId} not found.");
@@ -115,8 +125,8 @@ namespace RevitBridge.Tools
115
125
  throw new InvalidOperationException($"Parameter '{parameter.Definition?.Name}' is read-only on element {update.ElementId}.");
116
126
  }
117
127
 
118
- var before = GetElementDetails.DescribeParameter(doc, parameter, parameter.Definition?.Name ?? "", false);
119
- bool accepted = parameter.StorageType switch
128
+ var before = GetElementDetails.DescribeParameter(doc, parameter, parameter.Definition?.Name ?? "", false);
129
+ bool accepted = parameter.StorageType switch
120
130
  {
121
131
  StorageType.String => parameter.Set(ValueAsString(update.Value)),
122
132
  StorageType.Double => parameter.Set(ToInternalUnits(doc, parameter, RequireDouble(parameter, update.Value), update.Unit)),
@@ -131,38 +141,28 @@ namespace RevitBridge.Tools
131
141
  return SetElementName(element, update);
132
142
  throw new InvalidOperationException($"Revit rejected the value for '{parameter.Definition?.Name}' on element {update.ElementId} (Parameter.Set returned false).");
133
143
  }
134
- return new ValueChange(DescribeNewValue(parameter) ?? ValueAsString(update.Value), before,
135
- GetElementDetails.DescribeParameter(doc, parameter, parameter.Definition?.Name ?? "", false));
144
+ return new ValueChange(DescribeNewValue(parameter) ?? ValueAsString(update.Value), before,
145
+ GetElementDetails.DescribeParameter(doc, parameter, parameter.Definition?.Name ?? "", false));
136
146
  }
137
147
 
138
148
  /// <summary>Rename fallback: the Element.Name property setter.</summary>
139
- private static ValueChange SetElementName(Element element, Update update)
149
+ private static ValueChange SetElementName(Element element, Update update)
140
150
  {
141
151
  string name = ValueAsString(update.Value);
142
152
  if (string.IsNullOrWhiteSpace(name))
143
153
  throw new InvalidOperationException("Renaming needs a non-empty string value.");
144
- var before = new { value = element.Name, displayValue = element.Name, storageType = "String" };
145
- element.Name = name;
146
- return new ValueChange(name, before, new { value = element.Name, displayValue = element.Name, storageType = "String" });
154
+ var before = new { value = element.Name, displayValue = element.Name, storageType = "String" };
155
+ ElementNames.Assign(element, name);
156
+ return new ValueChange(name, before, new { value = element.Name, displayValue = element.Name, storageType = "String" });
147
157
  }
148
158
 
159
+ /// <summary>
160
+ /// Shared resolution policy (inv:parameter-ambiguity): a display name matching several
161
+ /// parameters on the element fails this update with the candidates' exact identities
162
+ /// instead of writing to an arbitrary one. Missing stays null.
163
+ /// </summary>
149
164
  private static Parameter? FindParameter(Element element, Update update)
150
- {
151
- if (update.BuiltIn is { } builtIn)
152
- return element.get_Parameter(builtIn);
153
- if (update.SharedGuid is { } guid)
154
- return element.get_Parameter(guid);
155
-
156
- var direct = element.LookupParameter(update.ParameterInput);
157
- if (direct != null)
158
- return direct;
159
- foreach (Parameter parameter in element.Parameters)
160
- {
161
- if (string.Equals(parameter.Definition?.Name, update.ParameterInput, StringComparison.OrdinalIgnoreCase))
162
- return parameter;
163
- }
164
- return null;
165
- }
165
+ => ParameterResolver.FindSingle(element, new ParameterReference(update.ParameterInput, update.BuiltIn, update.SharedGuid));
166
166
 
167
167
  private static string? DescribeNewValue(Parameter parameter)
168
168
  {
@@ -304,26 +304,7 @@ namespace RevitBridge.Tools
304
304
  if (!item.TryGetProperty("value", out var value))
305
305
  throw new ArgumentException($"The update for element {elementId} ('{parameterInput}') needs a value.");
306
306
 
307
- BuiltInParameter? builtIn = null;
308
- Guid? sharedGuid = null;
309
- if (parameterInput.StartsWith("guid:", StringComparison.OrdinalIgnoreCase))
310
- {
311
- sharedGuid = Guid.TryParse(parameterInput["guid:".Length..], out var guid)
312
- ? guid
313
- : throw new ArgumentException($"Invalid shared parameter guid: {parameterInput}");
314
- }
315
- else
316
- {
317
- string enumName = parameterInput.StartsWith("BuiltInParameter.", StringComparison.OrdinalIgnoreCase)
318
- ? parameterInput["BuiltInParameter.".Length..]
319
- : parameterInput;
320
- // BuiltInParameter names are SHOUTY_SNAKE_CASE; require an underscore or
321
- // all-caps so plain display names like "Comments" never collide.
322
- bool looksLikeEnumName = enumName.Length > 0 && char.IsLetter(enumName[0]) && !enumName.Contains(' ')
323
- && (enumName.Contains('_') || enumName.All(c => !char.IsLetter(c) || char.IsUpper(c)));
324
- if (looksLikeEnumName && Enum.TryParse<BuiltInParameter>(enumName, true, out var parsed) && parsed != BuiltInParameter.INVALID)
325
- builtIn = parsed;
326
- }
307
+ var reference = ParameterReference.Parse(parameterInput);
327
308
 
328
309
  updates.Add(new Update
329
310
  {
@@ -331,8 +312,8 @@ namespace RevitBridge.Tools
331
312
  ParameterInput = parameterInput,
332
313
  Value = value,
333
314
  Unit = JsonArgs.GetString(item, "unit"),
334
- BuiltIn = builtIn,
335
- SharedGuid = sharedGuid,
315
+ BuiltIn = reference.BuiltIn,
316
+ SharedGuid = reference.SharedGuid,
336
317
  });
337
318
  }
338
319
 
@@ -1,30 +1,30 @@
1
- using Autodesk.Revit.DB;
2
-
3
- namespace RevitBridge.Tools;
4
-
5
- internal sealed record SpatialBounds(XYZ Min, XYZ Max)
6
- {
7
- public static SpatialBounds? Of(Element element)
8
- {
9
- var box = element.get_BoundingBox(null);
10
- if (box == null) return null;
11
- var corners = new List<XYZ>(8);
12
- foreach (double x in new[] { box.Min.X, box.Max.X })
13
- foreach (double y in new[] { box.Min.Y, box.Max.Y })
14
- foreach (double z in new[] { box.Min.Z, box.Max.Z })
15
- corners.Add(box.Transform.OfPoint(new XYZ(x, y, z)));
16
- return new(new(corners.Min(p => p.X), corners.Min(p => p.Y), corners.Min(p => p.Z)),
17
- new(corners.Max(p => p.X), corners.Max(p => p.Y), corners.Max(p => p.Z)));
18
- }
19
-
20
- public bool Intersects(SpatialBounds other) => Min.X <= other.Max.X && Max.X >= other.Min.X
21
- && Min.Y <= other.Max.Y && Max.Y >= other.Min.Y && Min.Z <= other.Max.Z && Max.Z >= other.Min.Z;
22
- public bool Contains(SpatialBounds other) => Min.X <= other.Min.X && Max.X >= other.Max.X
23
- && Min.Y <= other.Min.Y && Max.Y >= other.Max.Y && Min.Z <= other.Min.Z && Max.Z >= other.Max.Z;
24
- public XYZ Gap(SpatialBounds other) => new(
25
- Math.Max(0, Math.Max(Min.X - other.Max.X, other.Min.X - Max.X)),
26
- Math.Max(0, Math.Max(Min.Y - other.Max.Y, other.Min.Y - Max.Y)),
27
- Math.Max(0, Math.Max(Min.Z - other.Max.Z, other.Min.Z - Max.Z)));
28
- public static double[] Coordinates(XYZ p, double scale) => new[] { p.X / scale, p.Y / scale, p.Z / scale };
29
- public object Describe(double scale) => new { min = Coordinates(Min, scale), max = Coordinates(Max, scale) };
30
- }
1
+ using Autodesk.Revit.DB;
2
+
3
+ namespace RevitBridge.Tools;
4
+
5
+ internal sealed record SpatialBounds(XYZ Min, XYZ Max)
6
+ {
7
+ public static SpatialBounds? Of(Element element)
8
+ {
9
+ var box = element.get_BoundingBox(null);
10
+ if (box == null) return null;
11
+ var corners = new List<XYZ>(8);
12
+ foreach (double x in new[] { box.Min.X, box.Max.X })
13
+ foreach (double y in new[] { box.Min.Y, box.Max.Y })
14
+ foreach (double z in new[] { box.Min.Z, box.Max.Z })
15
+ corners.Add(box.Transform.OfPoint(new XYZ(x, y, z)));
16
+ return new(new(corners.Min(p => p.X), corners.Min(p => p.Y), corners.Min(p => p.Z)),
17
+ new(corners.Max(p => p.X), corners.Max(p => p.Y), corners.Max(p => p.Z)));
18
+ }
19
+
20
+ public bool Intersects(SpatialBounds other) => Min.X <= other.Max.X && Max.X >= other.Min.X
21
+ && Min.Y <= other.Max.Y && Max.Y >= other.Min.Y && Min.Z <= other.Max.Z && Max.Z >= other.Min.Z;
22
+ public bool Contains(SpatialBounds other) => Min.X <= other.Min.X && Max.X >= other.Max.X
23
+ && Min.Y <= other.Min.Y && Max.Y >= other.Max.Y && Min.Z <= other.Min.Z && Max.Z >= other.Max.Z;
24
+ public XYZ Gap(SpatialBounds other) => new(
25
+ Math.Max(0, Math.Max(Min.X - other.Max.X, other.Min.X - Max.X)),
26
+ Math.Max(0, Math.Max(Min.Y - other.Max.Y, other.Min.Y - Max.Y)),
27
+ Math.Max(0, Math.Max(Min.Z - other.Max.Z, other.Min.Z - Max.Z)));
28
+ public static double[] Coordinates(XYZ p, double scale) => new[] { p.X / scale, p.Y / scale, p.Z / scale };
29
+ public object Describe(double scale) => new { min = Coordinates(Min, scale), max = Coordinates(Max, scale) };
30
+ }
@@ -1,87 +1,94 @@
1
- using System.Text.Json;
2
- using System.Text.Json.Nodes;
3
-
4
- namespace RevitBridge.Tools;
5
-
6
- internal sealed class SummarizeElements : ITool
7
- {
8
- private const int MaxElements = 10000;
9
- public string Name => "summarize_elements";
10
- public string Label => "Summarize Elements";
11
- public string Tier => "advanced";
12
- public string Description => "Count matching host-model elements grouped by category, type, level or one parameter value. Accepts get_elements category/class/level/type/filter scopes. At most 10,000 matches; narrow larger queries. Parameter grouping uses exact raw values, with a distinct missing-value group. Ambiguous display-name parameters are rejected. Counts cover the entire matching scope, not just the returned groups page.";
13
- public object ParametersSchema
14
- {
15
- get
16
- {
17
- var query = ElementQueryScope.Schema();
18
- return new
19
- {
20
- type = "object",
21
- properties = new
22
- {
23
- query,
24
- group_by = new { type = "string", @enum = new[] { "category", "typeName", "levelId", "parameter" } },
25
- parameter = new { type = "string", description = "Required for group_by=parameter. Name, built-in identity or guid:<GUID>." },
26
- type_parameter = new { type = "boolean", description = "Group by the parameter on the element type instead of the instance." },
27
- offset = new { type = "integer", minimum = 0 },
28
- limit = new { type = "integer", minimum = 1, maximum = 500 },
29
- },
30
- required = new[] { "group_by" },
31
- };
32
- }
33
- }
34
-
35
- public object Execute(JsonElement args, ToolContext context)
36
- {
37
- string groupBy = JsonArgs.GetString(args, "group_by") ?? "";
38
- if (groupBy is not ("category" or "typeName" or "levelId" or "parameter")) throw new ArgumentException("Unknown group_by value.");
39
- string? parameter = JsonArgs.GetString(args, "parameter");
40
- if (groupBy == "parameter" && string.IsNullOrWhiteSpace(parameter)) throw new ArgumentException("parameter is required for parameter grouping.");
41
- bool typeParameter = JsonArgs.GetBool(args, "type_parameter", false);
42
- var query = ElementQueryScope.Parse(args);
43
- JsonElement RunQuery() => JsonSerializer.SerializeToElement(((ToolOutput)new GetElements().Execute(JsonSerializer.SerializeToElement(query), context)!).Payload);
44
- query["count_only"] = true;
45
- var countResult = RunQuery();
46
- int total = countResult.GetProperty("total_count").GetInt32();
47
- object? warnings = countResult.TryGetProperty("warnings", out var queryWarnings) ? queryWarnings.Clone() : null;
48
- if (total > MaxElements) throw new ArgumentException($"Query matches {total} elements; narrow the scope to at most {MaxElements}.");
49
- query["count_only"] = false;
50
- query["limit"] = 1000;
51
- if (groupBy == "parameter")
52
- {
53
- query["parameter_names"] = new JsonArray(parameter);
54
- query["include_type_parameters"] = typeParameter;
55
- }
56
- var counts = new Dictionary<string, (object? Value, bool Missing, int Count)>();
57
- for (int position = 0; position < total; position += 1000)
58
- {
59
- query["offset"] = position;
60
- foreach (var row in RunQuery().GetProperty("elements").EnumerateArray())
61
- {
62
- object? value = null;
63
- bool missing = false;
64
- if (groupBy == "parameter")
65
- {
66
- var projection = row.GetProperty("parameters").EnumerateArray().FirstOrDefault(p => p.GetProperty("isType").GetBoolean() == typeParameter);
67
- missing = projection.ValueKind == JsonValueKind.Undefined || !projection.GetProperty("found").GetBoolean();
68
- if (!missing)
69
- {
70
- if (projection.GetProperty("ambiguous").GetBoolean()) throw new ArgumentException($"Parameter '{parameter}' is ambiguous on element {row.GetProperty("id")}; use a built-in identity or shared GUID.");
71
- value = projection.GetProperty("matches")[0].GetProperty("value").Clone();
72
- }
73
- }
74
- else if (row.TryGetProperty(groupBy, out var field)) value = field.Clone();
75
- string key = (missing ? "missing:" : "value:") + JsonSerializer.Serialize(value);
76
- counts[key] = counts.TryGetValue(key, out var previous) ? (value, missing, previous.Count + 1) : (value, missing, 1);
77
- }
78
- }
79
- int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
80
- int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 500);
81
- var groups = counts.OrderByDescending(pair => pair.Value.Count).ThenBy(pair => pair.Key, StringComparer.Ordinal).Skip(offset).Take(limit)
82
- .Select(pair => new { value = pair.Value.Value, missing = pair.Value.Missing, count = pair.Value.Count }).ToArray();
83
- return new { total_elements = total, total_groups = counts.Count, group_by = groupBy, parameter, type_parameter = typeParameter,
84
- numeric_values = "Revit internal units", warnings, offset, returned_count = groups.Length,
85
- next_offset = offset + groups.Length < counts.Count ? (int?)(offset + groups.Length) : null, groups };
86
- }
87
- }
1
+ using System.Text.Json;
2
+ using System.Text.Json.Nodes;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class SummarizeElements : ITool
7
+ {
8
+ private const int MaxElements = 10000;
9
+ public string Name => "summarize_elements";
10
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
11
+ public IReadOnlyList<string> Keywords => new[] { "statistics", "count by", "group by", "breakdown", "totals", "per level", "distribution" };
12
+ public IReadOnlyList<ToolLimit> Limits => new[]
13
+ {
14
+ new ToolLimit("Linked-model contents", "tool", "get_linked_elements"),
15
+ new ToolLimit("Scopes above 10,000 elements", "tool", "narrow the query, or get_elements with count_only for a total"),
16
+ };
17
+ public string Label => "Summarize Elements";
18
+ public string Tier => "advanced";
19
+ public string Description => "Count matching host-model elements grouped by category, type, level or one parameter value. Accepts get_elements category/class/level/type/filter scopes. At most 10,000 matches; narrow larger queries. Parameter grouping uses exact raw values, with a distinct missing-value group. Ambiguous display-name parameters are rejected. Counts cover the entire matching scope, not just the returned groups page.";
20
+ public object ParametersSchema
21
+ {
22
+ get
23
+ {
24
+ var query = ElementQueryScope.Schema();
25
+ return new
26
+ {
27
+ type = "object",
28
+ properties = new
29
+ {
30
+ query,
31
+ group_by = new { type = "string", @enum = new[] { "category", "typeName", "levelId", "parameter" } },
32
+ parameter = new { type = "string", description = "Required for group_by=parameter. Name, built-in identity or guid:<GUID>." },
33
+ type_parameter = new { type = "boolean", description = "Group by the parameter on the element type instead of the instance." },
34
+ offset = new { type = "integer", minimum = 0 },
35
+ limit = new { type = "integer", minimum = 1, maximum = 500 },
36
+ },
37
+ required = new[] { "group_by" },
38
+ };
39
+ }
40
+ }
41
+
42
+ public object Execute(JsonElement args, ToolContext context)
43
+ {
44
+ string groupBy = JsonArgs.GetString(args, "group_by") ?? "";
45
+ if (groupBy is not ("category" or "typeName" or "levelId" or "parameter")) throw new ArgumentException("Unknown group_by value.");
46
+ string? parameter = JsonArgs.GetString(args, "parameter");
47
+ if (groupBy == "parameter" && string.IsNullOrWhiteSpace(parameter)) throw new ArgumentException("parameter is required for parameter grouping.");
48
+ bool typeParameter = JsonArgs.GetBool(args, "type_parameter", false);
49
+ var query = ElementQueryScope.Parse(args);
50
+ JsonElement RunQuery() => JsonSerializer.SerializeToElement(((ToolOutput)new GetElements().Execute(JsonSerializer.SerializeToElement(query), context)!).Payload);
51
+ query["count_only"] = true;
52
+ var countResult = RunQuery();
53
+ int total = countResult.GetProperty("total_count").GetInt32();
54
+ object? warnings = countResult.TryGetProperty("warnings", out var queryWarnings) ? queryWarnings.Clone() : null;
55
+ if (total > MaxElements) throw new ArgumentException($"Query matches {total} elements; narrow the scope to at most {MaxElements}.");
56
+ query["count_only"] = false;
57
+ query["limit"] = 1000;
58
+ if (groupBy == "parameter")
59
+ {
60
+ query["parameter_names"] = new JsonArray(parameter);
61
+ query["include_type_parameters"] = typeParameter;
62
+ }
63
+ var counts = new Dictionary<string, (object? Value, bool Missing, int Count)>();
64
+ for (int position = 0; position < total; position += 1000)
65
+ {
66
+ query["offset"] = position;
67
+ foreach (var row in RunQuery().GetProperty("elements").EnumerateArray())
68
+ {
69
+ object? value = null;
70
+ bool missing = false;
71
+ if (groupBy == "parameter")
72
+ {
73
+ var projection = row.GetProperty("parameters").EnumerateArray().FirstOrDefault(p => p.GetProperty("isType").GetBoolean() == typeParameter);
74
+ missing = projection.ValueKind == JsonValueKind.Undefined || !projection.GetProperty("found").GetBoolean();
75
+ if (!missing)
76
+ {
77
+ if (projection.GetProperty("ambiguous").GetBoolean()) throw new ArgumentException($"Parameter '{parameter}' is ambiguous on element {row.GetProperty("id")}; use a built-in identity or shared GUID.");
78
+ value = projection.GetProperty("matches")[0].GetProperty("value").Clone();
79
+ }
80
+ }
81
+ else if (row.TryGetProperty(groupBy, out var field)) value = field.Clone();
82
+ string key = (missing ? "missing:" : "value:") + JsonSerializer.Serialize(value);
83
+ counts[key] = counts.TryGetValue(key, out var previous) ? (value, missing, previous.Count + 1) : (value, missing, 1);
84
+ }
85
+ }
86
+ int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
87
+ int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 500);
88
+ var groups = counts.OrderByDescending(pair => pair.Value.Count).ThenBy(pair => pair.Key, StringComparer.Ordinal).Skip(offset).Take(limit)
89
+ .Select(pair => new { value = pair.Value.Value, missing = pair.Value.Missing, count = pair.Value.Count }).ToArray();
90
+ return new { total_elements = total, total_groups = counts.Count, group_by = groupBy, parameter, type_parameter = typeParameter,
91
+ numeric_values = "Revit internal units", warnings, offset, returned_count = groups.Length,
92
+ next_offset = offset + groups.Length < counts.Count ? (int?)(offset + groups.Length) : null, groups };
93
+ }
94
+ }