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,64 +1,74 @@
1
- using System.Text.Json;
2
- using Autodesk.Revit.DB;
3
-
4
- namespace RevitBridge.Tools;
5
-
6
- /// <summary>Opaque identity of one open native document in this loaded bridge session.</summary>
7
- internal static class DocumentGuard
8
- {
9
- private sealed record Identity(Document Document, string Value);
10
- private static readonly string Generation = Guid.NewGuid().ToString("N");
11
- private static readonly List<Identity> Identities = new();
12
-
13
- public static string GetIdentity(Document document)
14
- {
15
- if (!document.IsValidObject)
16
- throw new ArgumentException("The target document is closed or invalid. Read get_model_overview again.");
17
- // Revit can supply multiple managed wrappers for the same native document.
18
- // Its documented Equals semantics identify that native document; reference identity cannot.
19
- // All accesses happen on the Revit API thread. Pruning avoids retaining closed documents.
20
- Identities.RemoveAll(entry => !entry.Document.IsValidObject);
21
- var existing = Identities.FirstOrDefault(entry => entry.Document.Equals(document));
22
- if (existing != null) return existing.Value;
23
- var created = new Identity(document, Generation + ":" + Guid.NewGuid().ToString("N"));
24
- Identities.Add(created);
25
- return created.Value;
26
- }
27
-
28
- public static bool AlwaysRequiresIdentity(string toolName)
29
- => toolName is "set_parameters" or "execute_csharp" or "export_documents" or "open_view";
30
-
31
- /// <summary>Must run on the Revit API thread immediately before the tool action.</summary>
32
- public static void CheckForTool(JsonElement args, Document document, string toolName, bool writes = false)
33
- {
34
- bool required = writes || AlwaysRequiresIdentity(toolName)
35
- || (toolName == "manage_selection"
36
- && (!string.Equals((JsonArgs.GetString(args, "action") ?? "get").Trim(), "get", StringComparison.OrdinalIgnoreCase)
37
- || JsonArgs.GetBool(args, "isolate_in_view", false)));
38
- if (required && !args.TryGetProperty("expected_document_id", out _))
39
- throw new ArgumentException("expected_document_id is required for this operation. Read project.documentId from get_model_overview for the intended open document and pass it unchanged. A title alone cannot identify a document. No action was performed.");
40
- CheckExpectedDocument(args, document);
41
- }
42
-
43
- public static void CheckExpectedDocument(JsonElement args, Document document)
44
- {
45
- if (args.TryGetProperty("expected_document_id", out var id))
46
- {
47
- if (id.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(id.GetString()))
48
- throw new ArgumentException("expected_document_id must be a non-empty identity from get_model_overview. No action was performed.");
49
- if (!string.Equals(id.GetString(), GetIdentity(document), StringComparison.Ordinal))
50
- throw new ArgumentException($"The active document '{document.Title}' is not the exact open document this call expected, or that identity is stale after reopening/restarting. No action was performed. Activate the intended document and read get_model_overview again.");
51
- }
52
-
53
- // Retained as an additional human-readable check, never as a substitute for identity.
54
- string? expected = JsonArgs.GetString(args, "expected_document");
55
- if (string.IsNullOrWhiteSpace(expected)) return;
56
- static string Normalize(string title)
57
- {
58
- string value = title.Trim();
59
- return value.EndsWith(".rvt", StringComparison.OrdinalIgnoreCase) ? value[..^4] : value;
60
- }
61
- if (!string.Equals(Normalize(expected), Normalize(document.Title), StringComparison.OrdinalIgnoreCase))
62
- throw new ArgumentException($"Active document is '{document.Title}' but this call expected title '{expected}'. No action was performed. Read get_model_overview for the intended document.");
63
- }
64
- }
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ /// <summary>Opaque identity of one open native document in this loaded bridge session.</summary>
7
+ internal static class DocumentGuard
8
+ {
9
+ private sealed record Identity(Document Document, string Value);
10
+ private static readonly string Generation = Guid.NewGuid().ToString("N");
11
+ private static readonly List<Identity> Identities = new();
12
+
13
+ public static string GetIdentity(Document document)
14
+ {
15
+ if (!document.IsValidObject)
16
+ throw new ArgumentException("The target document is closed or invalid. Read get_model_overview again.");
17
+ // Revit can supply multiple managed wrappers for the same native document.
18
+ // Its documented Equals semantics identify that native document; reference identity cannot.
19
+ // All accesses happen on the Revit API thread. Pruning avoids retaining closed documents.
20
+ Identities.RemoveAll(entry => !entry.Document.IsValidObject);
21
+ var existing = Identities.FirstOrDefault(entry => entry.Document.Equals(document));
22
+ if (existing != null) return existing.Value;
23
+ var created = new Identity(document, Generation + ":" + Guid.NewGuid().ToString("N"));
24
+ Identities.Add(created);
25
+ return created.Value;
26
+ }
27
+
28
+ public static bool AlwaysRequiresIdentity(string toolName)
29
+ => toolName is "set_parameters" or "execute_csharp" or "export_documents" or "open_view";
30
+
31
+ /// <summary>Project or family: the kind of the active document, as tools declare it.</summary>
32
+ public static string KindOf(Document document) => document.IsFamilyDocument ? DocumentKind.Family : DocumentKind.Project;
33
+
34
+ /// <summary>Refuse a tool in a document kind it does not declare, before it runs (inv:document-kind-declared).</summary>
35
+ public static void CheckKind(ITool tool, Document document)
36
+ {
37
+ if (DocumentKind.Refusal(tool.Name, tool.DocumentKinds, KindOf(document), document.Title) is { } refusal)
38
+ throw new ArgumentException(refusal);
39
+ }
40
+
41
+ /// <summary>Must run on the Revit API thread immediately before the tool action.</summary>
42
+ public static void CheckForTool(JsonElement args, Document document, string toolName, bool writes = false)
43
+ {
44
+ bool required = writes || AlwaysRequiresIdentity(toolName)
45
+ || (toolName == "manage_selection"
46
+ && (!string.Equals((JsonArgs.GetString(args, "action") ?? "get").Trim(), "get", StringComparison.OrdinalIgnoreCase)
47
+ || JsonArgs.GetBool(args, "isolate_in_view", false)));
48
+ if (required && !args.TryGetProperty("expected_document_id", out _))
49
+ throw new ArgumentException("expected_document_id is required for this operation. Read project.documentId from get_model_overview for the intended open document and pass it unchanged. A title alone cannot identify a document. No action was performed.");
50
+ CheckExpectedDocument(args, document);
51
+ }
52
+
53
+ public static void CheckExpectedDocument(JsonElement args, Document document)
54
+ {
55
+ if (args.TryGetProperty("expected_document_id", out var id))
56
+ {
57
+ if (id.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(id.GetString()))
58
+ throw new ArgumentException("expected_document_id must be a non-empty identity from get_model_overview. No action was performed.");
59
+ if (!string.Equals(id.GetString(), GetIdentity(document), StringComparison.Ordinal))
60
+ throw new ArgumentException($"The active document '{document.Title}' is not the exact open document this call expected, or that identity is stale after reopening/restarting. No action was performed. Activate the intended document and read get_model_overview again.");
61
+ }
62
+
63
+ // Retained as an additional human-readable check, never as a substitute for identity.
64
+ string? expected = JsonArgs.GetString(args, "expected_document");
65
+ if (string.IsNullOrWhiteSpace(expected)) return;
66
+ static string Normalize(string title)
67
+ {
68
+ string value = title.Trim();
69
+ return value.EndsWith(".rvt", StringComparison.OrdinalIgnoreCase) ? value[..^4] : value;
70
+ }
71
+ if (!string.Equals(Normalize(expected), Normalize(document.Title), StringComparison.OrdinalIgnoreCase))
72
+ throw new ArgumentException($"Active document is '{document.Title}' but this call expected title '{expected}'. No action was performed. Read get_model_overview for the intended document.");
73
+ }
74
+ }
@@ -0,0 +1,103 @@
1
+ using Autodesk.Revit.DB;
2
+
3
+ namespace RevitBridge.Tools
4
+ {
5
+ /// <summary>
6
+ /// The one place a tool assigns a name or sheet number (inv:existing-objects-not-reused).
7
+ /// A name another object of the same kind already uses is rejected before Revit is asked,
8
+ /// with the existing object's identity in the failure, so the caller learns that the object
9
+ /// predates this call instead of silently reusing or editing it. There is deliberately no
10
+ /// "reuse" option: reusing someone else's object needs the user's decision, after which the
11
+ /// caller works with that object's ID directly. The gate in check-tool-documentation.mjs
12
+ /// rejects any other Name or SheetNumber assignment in a tool.
13
+ /// </summary>
14
+ internal static class ElementNames
15
+ {
16
+ public const string CollisionKey = "name_collision";
17
+
18
+ /// <summary>Rename element, or fail with the colliding object's identity.</summary>
19
+ public static void Assign(Element element, string name)
20
+ {
21
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must be nonempty.");
22
+ if (element.Name == name) return;
23
+ if (RequiresUniqueName(element) && FindSameKind(element, name) is { } existing) throw Collision(element, existing, "name", name);
24
+ element.Name = name;
25
+ }
26
+
27
+ /// <summary>
28
+ /// Kinds whose names Revit keeps unique. Sheets (identified by number), rooms and other
29
+ /// objects may legitimately share a name, so they are not checked here.
30
+ /// </summary>
31
+ public static bool RequiresUniqueName(Element element) =>
32
+ element is (View and not ViewSheet) or Level or Grid or ElementType or Material or ParameterFilterElement;
33
+
34
+ /// <summary>Set a sheet number, or fail with the sheet that already has it.</summary>
35
+ public static void AssignSheetNumber(ViewSheet sheet, string number)
36
+ {
37
+ if (string.IsNullOrWhiteSpace(number)) throw new ArgumentException("number must be nonempty.");
38
+ if (sheet.SheetNumber == number) return;
39
+ var existing = new FilteredElementCollector(sheet.Document).OfClass(typeof(ViewSheet)).Cast<ViewSheet>()
40
+ .FirstOrDefault(other => other.Id != sheet.Id && other.SheetNumber == number);
41
+ if (existing != null) throw Collision(sheet, existing, "sheet number", number);
42
+ sheet.SheetNumber = number;
43
+ }
44
+
45
+ /// <summary>Create a family type in a family document, or fail when the name is taken.</summary>
46
+ public static FamilyType NewFamilyType(FamilyManager manager, string name)
47
+ {
48
+ EnsureFamilyTypeFree(manager, name);
49
+ return manager.NewType(name);
50
+ }
51
+
52
+ /// <summary>Rename the current family type, or fail when another type has the name.</summary>
53
+ public static void RenameCurrentFamilyType(FamilyManager manager, string name)
54
+ {
55
+ if (manager.CurrentType?.Name == name) return;
56
+ EnsureFamilyTypeFree(manager, name);
57
+ manager.RenameCurrentType(name);
58
+ }
59
+
60
+ /// <summary>Family types are not elements and have no ID; the collision names the type instead.</summary>
61
+ private static void EnsureFamilyTypeFree(FamilyManager manager, string name)
62
+ {
63
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must be nonempty.");
64
+ if (!manager.Types.Cast<FamilyType>().Any(type => type.Name == name)) return;
65
+ var error = new ArgumentException(Message("family type", "name", name, null));
66
+ error.Data[CollisionKey] = new Dictionary<string, object?> { ["existing_id"] = null, ["kind"] = "family type", ["name"] = name };
67
+ throw error;
68
+ }
69
+
70
+ /// <summary>
71
+ /// An object of the same kind with this exact name: same class, and for views the same
72
+ /// view type (a floor plan and a ceiling plan may share a name). Null when the name is free.
73
+ /// </summary>
74
+ public static Element? FindSameKind(Element element, string name)
75
+ {
76
+ FilteredElementCollector collector;
77
+ // Some API classes (for example Room) are not native filter classes; Revit's own
78
+ // uniqueness check still applies to them when the name is assigned.
79
+ try { collector = new FilteredElementCollector(element.Document).OfClass(element.GetType()); }
80
+ catch (Autodesk.Revit.Exceptions.ArgumentException) { return null; }
81
+ return collector.FirstOrDefault(other => other.Id != element.Id && other.Name == name
82
+ && (element is not View view || (other is View otherView && otherView.ViewType == view.ViewType && otherView.IsTemplate == view.IsTemplate))
83
+ && (element is not ElementType || other.Category?.Id == element.Category?.Id));
84
+ }
85
+
86
+ /// <summary>Pure message text, shared by every tool and tested offline.</summary>
87
+ public static string Message(string kind, string what, string value, long? existingId) =>
88
+ $"A {kind} with the {what} '{value}' already exists{(existingId is long id ? $" (id {id})" : "")}. Nothing was given that {what}. "
89
+ + "That object existed before this call: do not edit, reuse, replace or delete it unless the user asks. "
90
+ + "Ask the user, or choose a distinct value and report the collision.";
91
+
92
+ private static ArgumentException Collision(Element target, Element existing, string what, string value)
93
+ {
94
+ string kind = target is View view ? $"{view.ViewType} view" : target.GetType().Name;
95
+ var error = new ArgumentException(Message(kind, what, value, existing.Id.Value));
96
+ error.Data[CollisionKey] = new Dictionary<string, object?>
97
+ {
98
+ ["existing_id"] = existing.Id.Value, ["existing_unique_id"] = existing.UniqueId, ["kind"] = kind, [what.Replace(' ', '_')] = value,
99
+ };
100
+ return error;
101
+ }
102
+ }
103
+ }
@@ -1,27 +1,27 @@
1
- using System.Text.Json;
2
- using System.Text.Json.Nodes;
3
-
4
- namespace RevitBridge.Tools;
5
-
6
- internal static class ElementQueryScope
7
- {
8
- public static JsonObject Schema()
9
- {
10
- var schema = JsonSerializer.SerializeToNode(new GetElements().ParametersSchema)!.AsObject();
11
- foreach (string key in new[] { "count_only", "fields", "offset", "limit", "parameter_names", "include_type_parameters" })
12
- schema["properties"]!.AsObject().Remove(key);
13
- schema["description"] = "Filter the whole matching scope. Query paging and projections are not supported here.";
14
- schema["additionalProperties"] = false;
15
- return schema;
16
- }
17
-
18
- public static JsonObject Parse(JsonElement args)
19
- {
20
- var query = args.TryGetProperty("query", out var input) ? JsonNode.Parse(input.GetRawText()) as JsonObject : new JsonObject();
21
- if (query == null) throw new ArgumentException("query must be an object.");
22
- var allowed = Schema()["properties"]!.AsObject();
23
- foreach (var property in query)
24
- if (!allowed.ContainsKey(property.Key)) throw new ArgumentException($"query does not support '{property.Key}'. It always uses the whole matching scope.");
25
- return query;
26
- }
27
- }
1
+ using System.Text.Json;
2
+ using System.Text.Json.Nodes;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal static class ElementQueryScope
7
+ {
8
+ public static JsonObject Schema()
9
+ {
10
+ var schema = JsonSerializer.SerializeToNode(new GetElements().ParametersSchema)!.AsObject();
11
+ foreach (string key in new[] { "count_only", "fields", "offset", "limit", "parameter_names", "include_type_parameters" })
12
+ schema["properties"]!.AsObject().Remove(key);
13
+ schema["description"] = "Filter the whole matching scope. Query paging and projections are not supported here.";
14
+ schema["additionalProperties"] = false;
15
+ return schema;
16
+ }
17
+
18
+ public static JsonObject Parse(JsonElement args)
19
+ {
20
+ var query = args.TryGetProperty("query", out var input) ? JsonNode.Parse(input.GetRawText()) as JsonObject : new JsonObject();
21
+ if (query == null) throw new ArgumentException("query must be an object.");
22
+ var allowed = Schema()["properties"]!.AsObject();
23
+ foreach (var property in query)
24
+ if (!allowed.ContainsKey(property.Key)) throw new ArgumentException($"query does not support '{property.Key}'. It always uses the whole matching scope.");
25
+ return query;
26
+ }
27
+ }
@@ -0,0 +1,53 @@
1
+ using Autodesk.Revit.DB;
2
+
3
+ namespace RevitBridge.Tools
4
+ {
5
+ /// <summary>
6
+ /// Shared classification of special or system-owned objects (inv:special-objects-flagged).
7
+ /// Tools never mix these silently into ordinary results: they either flag them here or
8
+ /// exclude them and count the exclusion. Only non-default traits are emitted, so
9
+ /// ordinary elements add nothing to a result. New tools reuse this instead of inventing
10
+ /// their own checks, and new trait kinds are added here once.
11
+ /// </summary>
12
+ internal static class ElementTraits
13
+ {
14
+ /// <summary>Non-default traits of an element, or null when it has none.</summary>
15
+ public static Dictionary<string, object>? For(Element element)
16
+ {
17
+ var traits = new Dictionary<string, object>();
18
+ if (element is ScheduleSheetInstance { IsTitleblockRevisionSchedule: true })
19
+ traits["titleblock_revision_schedule"] = true;
20
+ if (element is ViewSchedule { IsTitleblockRevisionSchedule: true })
21
+ traits["titleblock_revision_schedule"] = true;
22
+ if (element is ViewSheet { IsPlaceholder: true })
23
+ traits["placeholder_sheet"] = true;
24
+ if (element is View view)
25
+ {
26
+ if (view.IsTemplate)
27
+ traits["view_template"] = true;
28
+ var primary = view.GetPrimaryViewId();
29
+ if (primary != null && primary != ElementId.InvalidElementId)
30
+ traits["dependent_view_of"] = primary.Value;
31
+ }
32
+ if (element.GroupId is { } group && group != ElementId.InvalidElementId)
33
+ traits["group_id"] = group.Value;
34
+ if (element.DesignOption is { } option)
35
+ traits["design_option_id"] = option.Id.Value;
36
+ if (element.Pinned)
37
+ traits["pinned"] = true;
38
+ return traits.Count > 0 ? traits : null;
39
+ }
40
+
41
+ /// <summary>
42
+ /// Kind of a sheet placement. A titleblock revision schedule is part of the titleblock,
43
+ /// not content someone placed, so it has its own kind and cannot be moved.
44
+ /// </summary>
45
+ public static string PlacementKind(Element placement) => placement switch
46
+ {
47
+ Viewport => "viewport",
48
+ ScheduleSheetInstance { IsTitleblockRevisionSchedule: true } => "titleblock_revision_schedule",
49
+ ScheduleSheetInstance => "schedule",
50
+ _ => "other",
51
+ };
52
+ }
53
+ }
@@ -20,12 +20,12 @@ namespace RevitBridge.Tools
20
20
  /// </summary>
21
21
  public sealed class ScriptGlobals
22
22
  {
23
- internal ScriptGlobals(Document document, UIDocument uiDocument, UIApplication uiApplication, JsonElement inputValues, Action<object?> dump)
23
+ internal ScriptGlobals(Document document, UIDocument uiDocument, UIApplication uiApplication, JsonElement inputValues, Action<object?> dump)
24
24
  {
25
25
  doc = document;
26
26
  uidoc = uiDocument;
27
- uiapp = uiApplication;
28
- inputs = inputValues;
27
+ uiapp = uiApplication;
28
+ inputs = inputValues;
29
29
  Dump = dump;
30
30
  }
31
31
 
@@ -36,10 +36,10 @@ namespace RevitBridge.Tools
36
36
  public UIDocument uidoc { get; }
37
37
 
38
38
  /// <summary>The Revit UI application.</summary>
39
- public UIApplication uiapp { get; }
40
-
41
- /// <summary>Structured inputs supplied separately from the script source.</summary>
42
- public JsonElement inputs { get; }
39
+ public UIApplication uiapp { get; }
40
+
41
+ /// <summary>Structured inputs supplied separately from the script source.</summary>
42
+ public JsonElement inputs { get; }
43
43
 
44
44
  /// <summary>Records a value into the result's dumps[] (safely projected at call time).</summary>
45
45
  public Action<object?> Dump { get; }
@@ -62,22 +62,31 @@ namespace RevitBridge.Tools
62
62
  private const int MaxReportedErrors = 20;
63
63
 
64
64
  public string Name => "execute_csharp";
65
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
66
+ public IReadOnlyList<string> Keywords => new[] { "script", "code", "csharp", "c#", "custom", "automation", "macro" };
67
+ public IReadOnlyList<ToolLimit> Limits => new[]
68
+ {
69
+ new ToolLimit("Automatic preview or rollback of custom code", "tool", "dedicated tools with preview (set_parameters, transform_elements, delete_elements, manage_views, ...)"),
70
+ new ToolLimit("Undoing file, UI or external effects on rollback", "user", "Inspect and clean up those effects explicitly"),
71
+ new ToolLimit("Saving the model", "user", "Save only when the user asks"),
72
+ };
73
+ public string? Verification => "reread";
65
74
  public string Label => "Execute C#";
66
- public string Description => "Compile and run a C# script on the Revit API thread against the open model — the escape hatch for anything without a dedicated tool: element creation, deletion, geometry edits (move/copy/rotate), views, sheets, schedules, tagging, families, links, worksets. Globals: doc (Document), uidoc (UIDocument), uiapp (UIApplication), inputs (System.Text.Json.JsonElement, defaults to an empty object), and Dump(value) to record intermediate values into the result's dumps[]. Default imports: System, System.Linq, System.Collections.Generic, Autodesk.Revit.DB, Autodesk.Revit.UI — add using directives at the top for sub-namespaces (e.g. using Autodesk.Revit.DB.Architecture;). The entire run is wrapped in ONE transaction named 'execute_csharp': committed on success; on failure rollback is attempted and its confirmed status is reported (do not open your own Transaction; sub-transactions are fine). The script's final expression or return statement becomes returnValue; return primitives, strings, or anonymous objects/lists — Revit API values are projected to safe shapes (Element -> {id,name,category,typeName,levelId}, ElementId -> number, XYZ -> {x,y,z}, Parameter -> {name,value,displayValue}; other API objects become strings) with depth and item caps, so never rely on raw API objects round-tripping. Lengths are in internal units (decimal feet) — convert with UnitUtils. Prefer collector-level filtering (FilteredElementCollector .OfCategory/.OfClass/.WhereElementIsNotElementType) and bounded loops: the call budget is 120s and Revit cannot be interrupted mid-script. Scripts must be fully synchronous — await/async is rejected at compile time, and blocking on tasks (Task.Result/.Wait()) can freeze Revit. Modal dialogs raised while running are auto-dismissed and reported in suppressedDialogs — unrecognized dialogs are answered dismissively (Cancel/Close/No) rather than confirmed, so an operation that raises a confirmation prompt may be cancelled; check suppressedDialogs when a result looks incomplete. Verify unfamiliar signatures with search_api_docs first.";
67
- public bool Write => true;
68
- public IReadOnlyList<string> Effects => new[] { "model", "ui", "files", "external" };
75
+ public string Description => "Compile and run a C# script on the Revit API thread against the open model, for operations that no dedicated tool covers. Specialist tools start inactive, so resolve the capability with find_revit_tools first and follow a tool's declared limits and alternatives. Globals: doc (Document), uidoc (UIDocument), uiapp (UIApplication), inputs (System.Text.Json.JsonElement, defaults to an empty object), and Dump(value) to record intermediate values into the result's dumps[]. Default imports: System, System.Linq, System.Collections.Generic, Autodesk.Revit.DB, Autodesk.Revit.UI — add using directives at the top for sub-namespaces (e.g. using Autodesk.Revit.DB.Architecture;). The entire run is wrapped in ONE transaction named 'execute_csharp': committed on success; on failure rollback is attempted and its confirmed status is reported (do not open your own Transaction; sub-transactions are fine). The script's final expression or return statement becomes returnValue; return primitives, strings, or anonymous objects/lists — Revit API values are projected to safe shapes (Element -> {id,name,category,typeName,levelId}, ElementId -> number, XYZ -> {x,y,z}, Parameter -> {name,value,displayValue}; other API objects become strings) with depth and item caps, so never rely on raw API objects round-tripping. Lengths are in internal units (decimal feet) — convert with UnitUtils. Prefer collector-level filtering (FilteredElementCollector .OfCategory/.OfClass/.WhereElementIsNotElementType) and bounded loops: the call budget is 120s and Revit cannot be interrupted mid-script. Scripts must be fully synchronous — await/async is rejected at compile time, and blocking on tasks (Task.Result/.Wait()) can freeze Revit. Modal dialogs raised while running are auto-dismissed and reported in suppressedDialogs — unrecognized dialogs are answered dismissively (Cancel/Close/No) rather than confirmed, so an operation that raises a confirmation prompt may be cancelled; check suppressedDialogs when a result looks incomplete. Verify unfamiliar signatures with search_api_docs first.";
76
+ public bool Write => true;
77
+ public IReadOnlyList<string> Effects => new[] { "model", "ui", "files", "external" };
69
78
 
70
79
  public object ParametersSchema => new
71
80
  {
72
81
  type = "object",
73
82
  properties = new
74
83
  {
75
- code = new
84
+ code = new
76
85
  {
77
86
  type = "string",
78
87
  description = "C# script body (top-level statements; using directives allowed at the top). Globals doc/uidoc/uiapp/inputs and Dump(value) are in scope. The final expression or a return statement is the result.",
79
- },
80
- inputs = new { type = "object", description = "Optional structured JSON inputs, available as the inputs JsonElement global. Default empty object. Values are never interpolated into source code." },
88
+ },
89
+ inputs = new { type = "object", description = "Optional structured JSON inputs, available as the inputs JsonElement global. Default empty object. Values are never interpolated into source code." },
81
90
  expected_document = new
82
91
  {
83
92
  type = "string",
@@ -87,10 +96,10 @@ namespace RevitBridge.Tools
87
96
  required = new[] { "code" },
88
97
  };
89
98
 
90
- public string? PromptSnippet => "Run a C# script against the Revit API inside one transaction (globals doc/uidoc/uiapp, Dump helper) — the escape hatch for operations without a dedicated tool.";
99
+ public string? PromptSnippet => "Run a C# script against the Revit API inside one transaction (globals doc/uidoc/uiapp, Dump helper) for operations no dedicated tool covers (check find_revit_tools first).";
91
100
  public IReadOnlyList<string>? PromptGuidelines => new[]
92
101
  {
93
- "Use execute_csharp for Revit operations no dedicated tool covers (creation, deletion, geometry edits, views/sheets, tagging, ...); check exact signatures with search_api_docs before writing the script.",
102
+ "Use execute_csharp only when no dedicated tool covers the operation (check find_revit_tools and the tool's declared limits first); verify exact signatures with search_api_docs before writing the script.",
94
103
  "execute_csharp scripts must be fully synchronous (await/async is rejected; never block on Task.Result/.Wait()) and should return primitives or anonymous objects/lists, using Dump(...) for intermediates — raw Revit objects are projected to compact summaries; keep loops bounded and filter at the collector level.",
95
104
  };
96
105
 
@@ -102,11 +111,11 @@ namespace RevitBridge.Tools
102
111
  DocumentGuard.CheckExpectedDocument(args, doc);
103
112
 
104
113
  string code = JsonArgs.GetString(args, "code") ?? string.Empty;
105
- if (string.IsNullOrWhiteSpace(code))
106
- throw new ArgumentException("code must be a non-empty C# script.");
107
- var inputValues = args.TryGetProperty("inputs", out var suppliedInputs) ? suppliedInputs.Clone() : JsonSerializer.SerializeToElement(new { });
108
- if (inputValues.ValueKind != JsonValueKind.Object) throw new ArgumentException("inputs must be a JSON object.");
109
- if (inputValues.GetRawText().Length > 100000) throw new ArgumentException("inputs must not exceed 100,000 JSON characters.");
114
+ if (string.IsNullOrWhiteSpace(code))
115
+ throw new ArgumentException("code must be a non-empty C# script.");
116
+ var inputValues = args.TryGetProperty("inputs", out var suppliedInputs) ? suppliedInputs.Clone() : JsonSerializer.SerializeToElement(new { });
117
+ if (inputValues.ValueKind != JsonValueKind.Object) throw new ArgumentException("inputs must be a JSON object.");
118
+ if (inputValues.GetRawText().Length > 100000) throw new ArgumentException("inputs must not exceed 100,000 JSON characters.");
110
119
 
111
120
  var stopwatch = Stopwatch.StartNew();
112
121
 
@@ -118,7 +127,7 @@ namespace RevitBridge.Tools
118
127
  RejectAsyncCode(script.GetCompilation());
119
128
 
120
129
  var dumps = new List<object?>();
121
- var globals = new ScriptGlobals(doc, uidoc, uiapp, inputValues, value =>
130
+ var globals = new ScriptGlobals(doc, uidoc, uiapp, inputValues, value =>
122
131
  {
123
132
  if (dumps.Count < MaxDumps)
124
133
  dumps.Add(Project(value, 0));
@@ -128,9 +137,9 @@ namespace RevitBridge.Tools
128
137
 
129
138
  using var dialogGuard = new DialogGuard(uiapp);
130
139
  using var transaction = new Transaction(doc, "execute_csharp");
131
- if (transaction.Start() != TransactionStatus.Started)
132
- throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
133
- var failureGuard = FailureGuard.Attach(transaction);
140
+ if (transaction.Start() != TransactionStatus.Started)
141
+ throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
142
+ var failureGuard = FailureGuard.Attach(transaction);
134
143
 
135
144
  object? returnValue;
136
145
  string? projectionError = null;
@@ -156,24 +165,24 @@ namespace RevitBridge.Tools
156
165
  }
157
166
  catch (Exception ex)
158
167
  {
159
- throw new InvalidOperationException(
160
- FormatRuntimeError(ex, dialogGuard.Suppressed) + " " + FailureGuard.RollBackAndDescribe(transaction), ex);
161
- }
162
-
163
- try
164
- {
165
- var status = transaction.Commit();
166
- var finalStatus = transaction.GetStatus();
167
- if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
168
- throw new InvalidOperationException($"The execute_csharp commit returned {status}; current transaction status is {finalStatus}.");
169
- }
170
- catch (Exception ex)
171
- {
172
- throw new InvalidOperationException(
173
- $"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}"
174
- + failureGuard.DescribeErrors()
175
- + DescribeDialogs(dialogGuard.Suppressed), ex);
176
- }
168
+ throw new InvalidOperationException(
169
+ FormatRuntimeError(ex, dialogGuard.Suppressed) + " " + FailureGuard.RollBackAndDescribe(transaction), ex);
170
+ }
171
+
172
+ try
173
+ {
174
+ var status = transaction.Commit();
175
+ var finalStatus = transaction.GetStatus();
176
+ if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
177
+ throw new InvalidOperationException($"The execute_csharp commit returned {status}; current transaction status is {finalStatus}.");
178
+ }
179
+ catch (Exception ex)
180
+ {
181
+ throw new InvalidOperationException(
182
+ $"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}"
183
+ + failureGuard.DescribeErrors()
184
+ + DescribeDialogs(dialogGuard.Suppressed), ex);
185
+ }
177
186
 
178
187
  stopwatch.Stop();
179
188
 
@@ -203,8 +212,8 @@ namespace RevitBridge.Tools
203
212
  Assembly.Load("netstandard"),
204
213
  typeof(Enumerable).Assembly, // System.Linq
205
214
  typeof(Regex).Assembly, // System.Text.RegularExpressions
206
- typeof(Console).Assembly, // System.Console
207
- typeof(JsonElement).Assembly, // System.Text.Json
215
+ typeof(Console).Assembly, // System.Console
216
+ typeof(JsonElement).Assembly, // System.Text.Json
208
217
  typeof(Document).Assembly, // RevitAPI
209
218
  typeof(UIApplication).Assembly) // RevitAPIUI
210
219
  .WithImports("System", "System.Linq", "System.Collections.Generic", "Autodesk.Revit.DB", "Autodesk.Revit.UI")
@@ -251,7 +260,7 @@ namespace RevitBridge.Tools
251
260
  string line = TryGetScriptLine(ex) is { } scriptLine ? $" at script line {scriptLine}" : string.Empty;
252
261
  string inner = ex.InnerException is { } innerEx ? $" Inner: {innerEx.GetType().Name}: {innerEx.Message}" : string.Empty;
253
262
  return $"C# script threw {ex.GetType().Name}{line}: {ex.Message}.{inner}"
254
- + DescribeDialogs(suppressedDialogs);
263
+ + DescribeDialogs(suppressedDialogs);
255
264
  }
256
265
 
257
266
  private static string DescribeDialogs(IReadOnlyList<string> suppressed)