pi-revit 0.3.1 → 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 (120) hide show
  1. package/AGENTS.md +167 -0
  2. package/CHANGELOG.md +114 -42
  3. package/README.md +598 -138
  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 +231 -85
  12. package/extensions/pi-revit/instance-router.ts +86 -0
  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 +146 -0
  16. package/extensions/pi-revit/tool-catalog.ts +166 -0
  17. package/extensions/pi-revit/tool-documentation.ts +72 -0
  18. package/extensions/pi-revit/tool-schema.ts +8 -0
  19. package/package.json +65 -59
  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 +39 -63
  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 +40 -0
  31. package/skills/pi-revit/references/operation-recovery.md +33 -0
  32. package/skills/pi-revit/references/room-documentation.md +39 -0
  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 +75 -19
  73. package/src/Revit/OperationStore.cs +178 -0
  74. package/src/Revit/ToolRegistry.cs +61 -8
  75. package/src/Revit/Tools/CaptureView.cs +9 -0
  76. package/src/Revit/Tools/ChangeElementTypes.cs +74 -0
  77. package/src/Revit/Tools/ChangeSet.cs +39 -0
  78. package/src/Revit/Tools/CreateTags.cs +107 -0
  79. package/src/Revit/Tools/DeleteElements.cs +53 -0
  80. package/src/Revit/Tools/DocumentGuard.cs +12 -2
  81. package/src/Revit/Tools/ElementNames.cs +103 -0
  82. package/src/Revit/Tools/ElementQueryScope.cs +27 -0
  83. package/src/Revit/Tools/ElementTraits.cs +53 -0
  84. package/src/Revit/Tools/ExecuteCsharp.cs +26 -7
  85. package/src/Revit/Tools/ExportDocuments.cs +9 -0
  86. package/src/Revit/Tools/FailureGuard.cs +26 -26
  87. package/src/Revit/Tools/GetElementDetails.cs +28 -2
  88. package/src/Revit/Tools/GetElementRelationships.cs +82 -0
  89. package/src/Revit/Tools/GetElementTypes.cs +8 -0
  90. package/src/Revit/Tools/GetElements.cs +58 -55
  91. package/src/Revit/Tools/GetLinkedElements.cs +89 -0
  92. package/src/Revit/Tools/GetLinkedModels.cs +73 -0
  93. package/src/Revit/Tools/GetModelCoordinates.cs +56 -0
  94. package/src/Revit/Tools/GetModelHealth.cs +7 -0
  95. package/src/Revit/Tools/GetModelOverview.cs +187 -160
  96. package/src/Revit/Tools/GetScheduleFields.cs +44 -0
  97. package/src/Revit/Tools/GetSchedules.cs +96 -0
  98. package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
  99. package/src/Revit/Tools/InheritedState.cs +144 -0
  100. package/src/Revit/Tools/ManageElementSets.cs +114 -0
  101. package/src/Revit/Tools/ManageSchedules.cs +174 -0
  102. package/src/Revit/Tools/ManageSelection.cs +9 -0
  103. package/src/Revit/Tools/ManageSheetPlacements.cs +113 -0
  104. package/src/Revit/Tools/ManageSheets.cs +72 -0
  105. package/src/Revit/Tools/ManageViews.cs +115 -0
  106. package/src/Revit/Tools/MeasureGeometry.cs +60 -0
  107. package/src/Revit/Tools/ModelChanges.cs +154 -0
  108. package/src/Revit/Tools/ModelEditBatch.cs +105 -0
  109. package/src/Revit/Tools/ModelEditInputs.cs +49 -0
  110. package/src/Revit/Tools/OpenView.cs +8 -0
  111. package/src/Revit/Tools/ParameterResolver.cs +94 -0
  112. package/src/Revit/Tools/QuerySpatialElements.cs +70 -0
  113. package/src/Revit/Tools/SearchApiDocs.cs +72 -4
  114. package/src/Revit/Tools/SetParameters.cs +50 -119
  115. package/src/Revit/Tools/SpatialBounds.cs +30 -0
  116. package/src/Revit/Tools/SummarizeElements.cs +94 -0
  117. package/src/Revit/Tools/ToolContract.cs +48 -0
  118. package/src/Revit/Tools/ToolSupport.cs +4 -0
  119. package/src/Revit/Tools/TransformElements.cs +73 -0
  120. package/workspace/AGENTS.md +54 -48
@@ -34,6 +34,9 @@ namespace RevitBridge
34
34
  /// <summary>True only for tools that mutate the model. Write tools own their transaction.</summary>
35
35
  bool Write => false;
36
36
 
37
+ /// <summary>Potential effects, independent of whether this particular call changes anything.</summary>
38
+ IReadOnlyList<string> Effects => Write ? new[] { "model" } : Array.Empty<string>();
39
+
37
40
  /// <summary>
38
41
  /// False only for tools that never touch the Revit API. They skip the bridge's
39
42
  /// no-document 409 gate and run directly on the server task instead of the Revit
@@ -44,12 +47,37 @@ namespace RevitBridge
44
47
  /// <summary>Activation tier metadata: "core" (default) or "advanced".</summary>
45
48
  string Tier => "core";
46
49
 
50
+ /// <summary>
51
+ /// Document kinds the tool works in: DocumentKind.Both or DocumentKind.ProjectOnly. Every
52
+ /// bridge tool declares it (the documentation gate requires the declaration); the
53
+ /// dispatcher refuses other kinds before the tool runs.
54
+ /// </summary>
55
+ IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
56
+
47
57
  /// <summary>Optional one-line "Available tools" system prompt entry.</summary>
48
58
  string? PromptSnippet => null;
49
59
 
50
60
  /// <summary>Optional system prompt guideline bullets. Each bullet must name the tool.</summary>
51
61
  IReadOnlyList<string>? PromptGuidelines => null;
52
62
 
63
+ /// <summary>
64
+ /// English discovery terms beyond the name and description: synonyms, outcome words
65
+ /// ("screenshot"), verbs and Revit nouns. Other UI languages are mapped onto these
66
+ /// terms by discovery alias files, never by translating tool metadata.
67
+ /// </summary>
68
+ IReadOnlyList<string> Keywords => Array.Empty<string>();
69
+
70
+ /// <summary>Declared capability boundaries, each with an alternative route.</summary>
71
+ IReadOnlyList<ToolLimit> Limits => Array.Empty<ToolLimit>();
72
+
73
+ /// <summary>
74
+ /// Minimum sufficient check that the requested outcome happened: "reread" (query the
75
+ /// changed state again), "capture" (inspect an image of the visible result),
76
+ /// "inspect_output" (open the produced file) or "none" (no durable outcome to check).
77
+ /// Required when the tool can write or has effects; null for pure reads.
78
+ /// </summary>
79
+ string? Verification => null;
80
+
53
81
  /// <summary>
54
82
  /// Runs on the Revit API thread (or the server task when RequiresDocument is false).
55
83
  /// Returns a ToolOutput or any plain JSON-serializable payload.
@@ -77,6 +105,24 @@ namespace RevitBridge
77
105
  registry.Add(new CaptureView());
78
106
  registry.Add(new ExportDocuments());
79
107
  registry.Add(new GetModelHealth());
108
+ registry.Add(new GetLinkedModels());
109
+ registry.Add(new GetLinkedElements());
110
+ registry.Add(new GetSchedules());
111
+ registry.Add(new GetElementRelationships());
112
+ registry.Add(new SummarizeElements());
113
+ registry.Add(new ManageElementSets());
114
+ registry.Add(new TransformElements());
115
+ registry.Add(new Tools.DeleteElements());
116
+ registry.Add(new ChangeElementTypes());
117
+ registry.Add(new ManageViews());
118
+ registry.Add(new ManageSheets());
119
+ registry.Add(new ManageSheetPlacements());
120
+ registry.Add(new GetScheduleFields());
121
+ registry.Add(new ManageSchedules());
122
+ registry.Add(new CreateTags());
123
+ registry.Add(new QuerySpatialElements());
124
+ registry.Add(new MeasureGeometry());
125
+ registry.Add(new GetModelCoordinates());
80
126
  return registry;
81
127
  }
82
128
 
@@ -103,14 +149,17 @@ namespace RevitBridge
103
149
  parameters = DescribeParameters(tool),
104
150
  executionMode = "sequential",
105
151
  write = tool.Write,
152
+ effects = tool.Effects,
106
153
  requiresDocument = tool.RequiresDocument,
154
+ documentKinds = tool.DocumentKinds,
155
+ keywords = tool.Keywords,
156
+ limits = tool.Limits.Select(limit => new { what = limit.What, alternative = new { kind = limit.Alternative, @ref = limit.Ref } }).ToArray(),
157
+ verification = tool.Verification,
107
158
  promptSnippet = tool.PromptSnippet,
108
- promptGuidelines = tool.RequiresDocument
109
- ? (tool.PromptGuidelines ?? Array.Empty<string>()).Concat(new[]
110
- {
111
- $"{tool.Name}: use project.documentId from get_model_overview as expected_document_id to bind the call to that exact open document. It is required for model writes, open_view, and selection changes; legacy expected_document titles alone are insufficient. Refresh after closing/reopening or restarting Revit."
112
- }).ToArray()
113
- : tool.PromptGuidelines,
159
+ // Cross-cutting rules (document identity, manuals, capability and completion
160
+ // protocols) are stated once by the Pi extension's platform section, not
161
+ // repeated per tool; guidelines here are tool-specific.
162
+ promptGuidelines = tool.PromptGuidelines,
114
163
  };
115
164
 
116
165
  private static object DescribeParameters(ITool tool)
@@ -118,12 +167,16 @@ namespace RevitBridge
118
167
  if (!tool.RequiresDocument) return tool.ParametersSchema;
119
168
  var schema = JsonSerializer.SerializeToNode(tool.ParametersSchema)!.AsObject();
120
169
  var properties = schema["properties"]!.AsObject();
170
+ bool identityRequired = tool.Write || DocumentGuard.AlwaysRequiresIdentity(tool.Name);
171
+ // The description must agree with requiredness in the same schema.
121
172
  properties["expected_document_id"] = new JsonObject
122
173
  {
123
174
  ["type"] = "string",
124
- ["description"] = "Exact opaque project.documentId from get_model_overview. Required for document writes and UI mutations; optional for reads. Invalid after close/reopen or bridge restart."
175
+ ["description"] = identityRequired
176
+ ? "Required: exact opaque project.documentId from get_model_overview for the intended open document. Invalid after close/reopen or bridge restart."
177
+ : "Exact opaque project.documentId from get_model_overview. Not required by the schema for reads, but required at runtime for any model change or UI change this tool performs; when supplied, the call runs only against that document. Invalid after close/reopen or bridge restart."
125
178
  };
126
- if (DocumentGuard.AlwaysRequiresIdentity(tool.Name))
179
+ if (identityRequired)
127
180
  {
128
181
  var required = schema["required"] as JsonArray ?? new JsonArray();
129
182
  if (!required.Any(x => x?.GetValue<string>() == "expected_document_id")) required.Add("expected_document_id");
@@ -20,6 +20,15 @@ namespace RevitBridge.Tools
20
20
  private static readonly TimeSpan CaptureRetention = TimeSpan.FromHours(24);
21
21
 
22
22
  public string Name => "capture_view";
23
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
24
+ public IReadOnlyList<string> Keywords => new[] { "screenshot", "image", "png", "picture", "snapshot", "visual check" };
25
+ public IReadOnlyList<ToolLimit> Limits => new[]
26
+ {
27
+ new ToolLimit("Image contents or visual judgement (returns a file path only)", "tool", "read (open the returned PNG)"),
28
+ new ToolLimit("Deliverable PDF, DWG or IFC files", "tool", "export_documents"),
29
+ };
30
+ public string? Verification => "inspect_output";
31
+ public IReadOnlyList<string> Effects => new[] { "files" };
23
32
  public string Label => "Capture View";
24
33
  public string Description => "Export a PNG snapshot of a Revit view to a temporary file and return its path — the response contains NO image data; open the returned filePath with the read tool to actually see the image. Defaults to the active view; pass view_id for any other graphical view or sheet (find ids with get_elements, category 'Views' or 'Sheets'). The long image edge is capped at 1568 px. Schedules and view templates cannot be captured.";
25
34
  public string Tier => "advanced";
@@ -0,0 +1,74 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class ChangeElementTypes : ITool
7
+ {
8
+ /// <summary>Updates whose per-view graphics are scanned; each scan visits every view.</summary>
9
+ private const int ViewGraphicsLimit = 20;
10
+ public string Name => "change_element_types";
11
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
12
+ public IReadOnlyList<string> Keywords => new[] { "swap type", "replace type", "change family type", "retype", "switch type" };
13
+ public IReadOnlyList<ToolLimit> Limits => new[]
14
+ {
15
+ new ToolLimit("Editing a type definition's parameters", "tool", "set_parameters"),
16
+ new ToolLimit("Creating a new type", "api", "ElementType.Duplicate"),
17
+ new ToolLimit("Loading a family", "api", "Document.LoadFamily"),
18
+ };
19
+ public string? Verification => "reread";
20
+ public string Label => "Change Element Types";
21
+ public string Tier => "advanced";
22
+ public bool Write => true;
23
+ public string Description => "Change the type of 1–200 elements using explicit element_id/type_id pairs. Revit validates each type against the target; invalid or pinned targets are reported per update. Some type changes replace the original element: always use resulting_id/unique_id afterward. Each update reports inherited_state: instance values, traits and, for the first 20 updates, views that hide or override the retyped element. Default partial success; atomic=true rolls back all if any update fails. preview=true commit-validates then rolls back, and replacement IDs from previews are temporary. Revit constraints can affect connected/hosted elements; results describe requested targets, not every dependent effect.";
24
+ public object ParametersSchema => new
25
+ {
26
+ type = "object", properties = new
27
+ {
28
+ updates = new { type = "array", minItems = 1, maxItems = 200, items = new
29
+ {
30
+ type = "object", properties = new { element_id = new { type = "integer", minimum = 1 }, type_id = new { type = "integer", minimum = 1 } },
31
+ required = new[] { "element_id", "type_id" },
32
+ } },
33
+ preview = ModelEditInputs.PreviewSchema,
34
+ atomic = new { type = "boolean", description = "Roll back the complete batch if any update fails. Default false." },
35
+ }, required = new[] { "updates" },
36
+ };
37
+ public object Execute(JsonElement args, ToolContext context)
38
+ {
39
+ var doc = context.Document ?? throw new NoActiveDocumentException();
40
+ if (!args.TryGetProperty("updates", out var updates) || updates.ValueKind != JsonValueKind.Array || updates.GetArrayLength() is < 1 or > 200)
41
+ throw new ArgumentException("updates must contain 1–200 element_id/type_id pairs.");
42
+ var seen = new HashSet<long>();
43
+ var steps = new List<ModelEditBatch.Step>();
44
+ foreach (var update in updates.EnumerateArray())
45
+ {
46
+ long elementId = JsonArgs.GetLong(update, "element_id") ?? 0;
47
+ long typeId = JsonArgs.GetLong(update, "type_id") ?? 0;
48
+ if (elementId <= 0 || typeId <= 0 || !seen.Add(elementId)) throw new ArgumentException("Each update needs positive IDs, and element_id must be unique within the batch.");
49
+ int index = steps.Count;
50
+ steps.Add(new(new() { ["element_id"] = elementId, ["type_id"] = typeId }, () =>
51
+ {
52
+ var element = doc.GetElement(new ElementId(elementId)) ?? throw new ArgumentException($"Element {elementId} not found.");
53
+ if (element.Pinned) throw new ArgumentException("Target is pinned; it was not unpinned.");
54
+ var type = doc.GetElement(new ElementId(typeId)) as ElementType ?? throw new ArgumentException($"Type {typeId} not found.");
55
+ if (!element.IsValidType(type.Id)) throw new ArgumentException($"Type {typeId} is not valid for element {elementId}.");
56
+ var before = ModelEditInputs.Snapshot(element);
57
+ var replacement = element.ChangeTypeId(type.Id);
58
+ doc.Regenerate();
59
+ var resultId = replacement == ElementId.InvalidElementId ? new ElementId(elementId) : replacement;
60
+ var result = doc.GetElement(resultId) ?? throw new InvalidOperationException("Changed element could not be resolved.");
61
+ return new() { ["before"] = before, ["after"] = ModelEditInputs.Snapshot(result),
62
+ ["resulting_id"] = result.Id.Value, ["unique_id"] = result.UniqueId,
63
+ ["replaced"] = result.Id.Value != elementId,
64
+ ["resulting_id_is_temporary"] = result.Id.Value != elementId && JsonArgs.GetBool(args, "preview", false),
65
+ // What the retyped element keeps: instance values, traits and per-view graphics (inv:derived-state-reported).
66
+ ["inherited_state"] = InheritedState.OfElement(result, result.Id.Value != elementId ? elementId : null, viewGraphics: index < ViewGraphicsLimit) };
67
+ }));
68
+ }
69
+ var batch = ModelEditBatch.Run(doc, Name, args, steps);
70
+ foreach (var row in batch.Proposed)
71
+ if (row["replaced"] is true) row["resulting_id_is_temporary"] = true;
72
+ return batch.Payload;
73
+ }
74
+ }
@@ -0,0 +1,39 @@
1
+ namespace RevitBridge.Tools
2
+ {
3
+ /// <summary>
4
+ /// Net element changes of one tool call, from Revit's DocumentChanged operations. Pure, so it
5
+ /// is tested offline. A rolled-back or undone group discards what was recorded before it, so a
6
+ /// preview (commit inside a rolled-back group) reports no net change.
7
+ /// </summary>
8
+ internal sealed class ChangeSet
9
+ {
10
+ private readonly HashSet<long> _added = new(), _modified = new(), _deleted = new();
11
+ public bool Observed { get; private set; }
12
+ public bool RolledBack { get; private set; }
13
+ public IReadOnlyCollection<long> Added => _added;
14
+ public IReadOnlyCollection<long> Modified => _modified;
15
+ public IReadOnlyCollection<long> Deleted => _deleted;
16
+
17
+ public void Record(string operation, IEnumerable<long> added, IEnumerable<long> modified, IEnumerable<long> deleted)
18
+ {
19
+ Observed = true;
20
+ if (operation is not ("TransactionCommitted" or "TransactionRedone"))
21
+ {
22
+ // Undo or rollback: everything recorded in this call is no longer a net change.
23
+ _added.Clear(); _modified.Clear(); _deleted.Clear();
24
+ RolledBack = true;
25
+ return;
26
+ }
27
+ foreach (var id in added) { _added.Add(id); _deleted.Remove(id); }
28
+ foreach (var id in modified) if (!_added.Contains(id)) _modified.Add(id);
29
+ foreach (var id in deleted)
30
+ {
31
+ _modified.Remove(id);
32
+ // Created and deleted within one call leaves nothing behind.
33
+ if (!_added.Remove(id)) _deleted.Add(id);
34
+ }
35
+ }
36
+
37
+ public bool IsEmpty => _added.Count == 0 && _modified.Count == 0 && _deleted.Count == 0;
38
+ }
39
+ }
@@ -0,0 +1,107 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+ using Autodesk.Revit.DB.Architecture;
4
+ using Autodesk.Revit.DB.Mechanical;
5
+
6
+ namespace RevitBridge.Tools;
7
+
8
+ internal sealed class CreateTags : ITool
9
+ {
10
+ public string Name => "create_tags";
11
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.ProjectOnly;
12
+ public IReadOnlyList<string> Keywords => new[] { "tag", "label", "annotate", "annotation", "room tag", "door tag" };
13
+ public IReadOnlyList<ToolLimit> Limits => new[]
14
+ {
15
+ new ToolLimit("Tagging elements inside linked models", "api", "Reference.CreateLinkReference; IndependentTag.Create"),
16
+ new ToolLimit("Tagging faces or subelements", "api", "IndependentTag.Create with a face Reference"),
17
+ new ToolLimit("Creating or loading tag families", "api", "Document.LoadFamily"),
18
+ new ToolLimit("Text notes, dimensions and other annotation", "api", "TextNote.Create; Creation.ItemFactoryBase.NewDimension"),
19
+ };
20
+ public string? Verification => "capture";
21
+ public string Label => "Create Tags";
22
+ public string Tier => "advanced";
23
+ public bool Write => true;
24
+ public string Description => "Create up to 100 tags for host-document elements in one explicit view using a loaded tag FamilySymbol. kind=element uses IndependentTag; room, space and area use their corresponding spatial tag APIs. Targets contain element_id and head_position [x,y,z] in document internal coordinates with explicit length unit. head_position always means the tag head, including with leader=true. Spatial tags require a compatible plan view and positions at that spatial element's level; orientation applies only to element tags. Templates, perspective views and unlocked 3D views cannot host independent tags. Default partial success; atomic=true rolls back all on any failure. preview=true commit-validates then rolls back; all proposed tag IDs are temporary. Linked targets and face/subelement references are outside this tool.";
25
+ public object ParametersSchema => new
26
+ {
27
+ type = "object", properties = new
28
+ {
29
+ kind = new { type = "string", @enum = new[] { "element", "room", "space", "area" } },
30
+ view_id = new { type = "integer", minimum = 1 }, tag_type_id = new { type = "integer", minimum = 1 }, unit = ModelEditInputs.LengthUnitSchema,
31
+ targets = new { type = "array", minItems = 1, maxItems = 100, items = new { type = "object", properties = new { element_id = new { type = "integer", minimum = 1 }, head_position = ModelEditInputs.VectorSchema("Tag head in document internal coordinates, in unit.") }, required = new[] { "element_id", "head_position" } } },
32
+ leader = new { type = "boolean" }, orientation = new { type = "string", @enum = new[] { "horizontal", "vertical" } },
33
+ preview = ModelEditInputs.PreviewSchema, atomic = new { type = "boolean" },
34
+ }, required = new[] { "kind", "view_id", "tag_type_id", "unit", "targets" },
35
+ };
36
+ public object Execute(JsonElement args, ToolContext context)
37
+ {
38
+ var doc = context.Document ?? throw new NoActiveDocumentException();
39
+ string kind = JsonArgs.GetString(args, "kind") ?? "";
40
+ if (kind is not ("element" or "room" or "space" or "area")) throw new ArgumentException("Unknown tag kind.");
41
+ var view = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "view_id") ?? 0)) as View ?? throw new ArgumentException("view_id is not a view.");
42
+ if (view.IsTemplate || (view is View3D three && (three.IsPerspective || !three.IsLocked))) throw new ArgumentException("Tags require a non-template view; 3D views must be orthographic and locked.");
43
+ if (kind != "element" && view is not ViewPlan) throw new ArgumentException("Spatial tags require a plan view.");
44
+ var type = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "tag_type_id") ?? 0)) as FamilySymbol ?? throw new ArgumentException("tag_type_id must identify a loaded tag FamilySymbol.");
45
+ bool leader = false;
46
+ if (args.TryGetProperty("leader", out var leaderValue))
47
+ {
48
+ if (leaderValue.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new ArgumentException("leader must be boolean.");
49
+ leader = leaderValue.GetBoolean();
50
+ }
51
+ if (kind != "element" && args.TryGetProperty("orientation", out _)) throw new ArgumentException("orientation applies only to element tags.");
52
+ var orientation = (JsonArgs.GetString(args, "orientation") ?? "horizontal") switch
53
+ {
54
+ "horizontal" => TagOrientation.Horizontal, "vertical" => TagOrientation.Vertical, _ => throw new ArgumentException("Invalid orientation."),
55
+ };
56
+ if (!args.TryGetProperty("targets", out var targets) || targets.ValueKind != JsonValueKind.Array || targets.GetArrayLength() is < 1 or > 100) throw new ArgumentException("targets must contain 1–100 tag requests.");
57
+ double scale = ModelEditInputs.LengthScale(args);
58
+ var steps = new List<ModelEditBatch.Step>();
59
+ foreach (var input in targets.EnumerateArray())
60
+ {
61
+ if (input.ValueKind != JsonValueKind.Object) throw new ArgumentException("Each target must be an object.");
62
+ long id = JsonArgs.GetLong(input, "element_id") ?? 0;
63
+ if (id <= 0) throw new ArgumentException("Target element_id must be positive.");
64
+ var point = ModelEditInputs.Vector(input, "head_position").Multiply(scale);
65
+ steps.Add(new(new() { ["element_id"] = id }, () =>
66
+ {
67
+ var element = doc.GetElement(new ElementId(id)) ?? throw new ArgumentException($"Element {id} not found.");
68
+ if (!type.IsActive) { type.Activate(); doc.Regenerate(); }
69
+ Element tag;
70
+ if (kind == "element")
71
+ {
72
+ var independent = IndependentTag.Create(doc, type.Id, view.Id, new Reference(element), false, orientation, point);
73
+ independent.HasLeader = leader;
74
+ independent.TagHeadPosition = point;
75
+ tag = independent;
76
+ }
77
+ else
78
+ {
79
+ if (element.Location is not LocationPoint location || view.GenLevel == null || element.LevelId != view.GenLevel.Id)
80
+ throw new ArgumentException("A spatial tag target must be placed on the plan view's level.");
81
+ var anchor = new UV(location.Point.X, location.Point.Y);
82
+ SpatialElementTag spatial = kind switch
83
+ {
84
+ "room" when element is Room => doc.Create.NewRoomTag(new LinkElementId(element.Id), anchor, view.Id),
85
+ "space" when element is Space space => doc.Create.NewSpaceTag(space, anchor, view),
86
+ "area" when element is Area area => doc.Create.NewAreaTag((ViewPlan)view, area, anchor),
87
+ _ => throw new ArgumentException($"Element {id} does not match kind {kind}."),
88
+ };
89
+ if (!spatial.IsValidType(type.Id)) throw new ArgumentException("Tag type does not match the spatial tag category.");
90
+ var replacement = spatial.ChangeTypeId(type.Id);
91
+ if (replacement != ElementId.InvalidElementId) spatial = (SpatialElementTag)doc.GetElement(replacement);
92
+ spatial.HasLeader = leader; spatial.TagHeadPosition = point;
93
+ tag = spatial;
94
+ }
95
+ doc.Regenerate();
96
+ var actual = tag is IndependentTag independentResult ? independentResult.TagHeadPosition : ((SpatialElementTag)tag).TagHeadPosition;
97
+ return new() { ["tag_id"] = tag.Id.Value, ["unique_id"] = tag.UniqueId, ["tag_type_id"] = tag.GetTypeId().Value, ["view_id"] = tag.OwnerViewId.Value,
98
+ ["kind"] = kind, ["head_position"] = new[] { actual.X, actual.Y, actual.Z }, ["unit"] = "feet", ["leader"] = leader, ["id_is_temporary"] = false,
99
+ // A retyped spatial tag may be replaced by a new element (inv:derived-state-reported).
100
+ ["inherited_state"] = InheritedState.OfElement(tag, null) };
101
+ }));
102
+ }
103
+ var batch = ModelEditBatch.Run(doc, Name, args, steps);
104
+ foreach (var row in batch.Proposed) row["id_is_temporary"] = true;
105
+ return batch.Payload;
106
+ }
107
+ }
@@ -0,0 +1,53 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class DeleteElements : ITool
7
+ {
8
+ public string Name => "delete_elements";
9
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
10
+ public IReadOnlyList<string> Keywords => new[] { "remove", "erase", "cleanup", "delete view", "delete sheet" };
11
+ public IReadOnlyList<ToolLimit> Limits => new[]
12
+ {
13
+ new ToolLimit("Pinned elements (rejected, never unpinned automatically)", "user", "Confirm unpinning (Element.Pinned) first"),
14
+ new ToolLimit("Elements inside linked models", "user", "Edit the linked model itself"),
15
+ new ToolLimit("One-step purge of unused types", "tool", "get_element_types with include_instance_count, then delete_elements"),
16
+ };
17
+ public string? Verification => "reread";
18
+ public string Label => "Delete Elements";
19
+ public string Tier => "advanced";
20
+ public bool Write => true;
21
+ public string Description => "Delete 1–200 explicitly selected elements in one atomic step, returning the full set of IDs returned by Revit's deletion API, including dependents. Use preview=true to inspect that set with commit validation and confirmed rollback. Optional expected_deleted_ids rejects a changed deletion set before commit; pass the full preview set to bind a later deletion to that set. Pinned requested elements are rejected. At most 10,000 deleted IDs are permitted; a larger cascade rolls back. Returned IDs include dependencies removed by Document.Delete, not a complete audit of surviving elements modified by Revit.";
22
+ public object ParametersSchema => new
23
+ {
24
+ type = "object", properties = new
25
+ {
26
+ element_ids = ModelEditInputs.IdsSchema,
27
+ preview = ModelEditInputs.PreviewSchema,
28
+ expected_deleted_ids = new { type = "array", minItems = 1, maxItems = 10000, uniqueItems = true, items = new { type = "integer", minimum = 1 }, description = "Exact full deleted_ids set from a previous preview; mismatches roll back." },
29
+ }, required = new[] { "element_ids" },
30
+ };
31
+ public object Execute(JsonElement args, ToolContext context)
32
+ {
33
+ var doc = context.Document ?? throw new NoActiveDocumentException();
34
+ var ids = ModelEditInputs.Ids(args);
35
+ List<long>? expected = args.TryGetProperty("expected_deleted_ids", out _) ? JsonArgs.GetLongArray(args, "expected_deleted_ids") : null;
36
+ if (expected != null && (expected.Count is < 1 or > 10000 || expected.Any(x => x <= 0) || expected.Distinct().Count() != expected.Count))
37
+ throw new ArgumentException("expected_deleted_ids must contain 1–10,000 distinct positive IDs.");
38
+ return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(
39
+ new() { ["element_ids"] = ids.Select(x => x.Value).ToArray() }, () =>
40
+ {
41
+ foreach (var id in ids)
42
+ {
43
+ var element = doc.GetElement(id) ?? throw new ArgumentException($"Element {id.Value} not found.");
44
+ if (element.Pinned) throw new ArgumentException($"Element {id.Value} is pinned; no elements were unpinned.");
45
+ }
46
+ var deleted = doc.Delete(ids).Select(id => id.Value).OrderBy(id => id).ToArray();
47
+ if (deleted.Length > 10000) throw new ArgumentException("Deletion exceeds the 10,000-ID cascade limit.");
48
+ if (expected != null && !deleted.ToHashSet().SetEquals(expected)) throw new ArgumentException("The deletion set changed since the preview; deletion was rolled back. Run a fresh preview.");
49
+ return new() { ["deleted_ids"] = deleted, ["deleted_count"] = deleted.Length,
50
+ ["dependent_ids"] = deleted.Except(ids.Select(x => x.Value)).ToArray() };
51
+ }) }).Payload;
52
+ }
53
+ }
@@ -28,10 +28,20 @@ internal static class DocumentGuard
28
28
  public static bool AlwaysRequiresIdentity(string toolName)
29
29
  => toolName is "set_parameters" or "execute_csharp" or "export_documents" or "open_view";
30
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
+
31
41
  /// <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)
42
+ public static void CheckForTool(JsonElement args, Document document, string toolName, bool writes = false)
33
43
  {
34
- bool required = AlwaysRequiresIdentity(toolName)
44
+ bool required = writes || AlwaysRequiresIdentity(toolName)
35
45
  || (toolName == "manage_selection"
36
46
  && (!string.Equals((JsonArgs.GetString(args, "action") ?? "get").Trim(), "get", StringComparison.OrdinalIgnoreCase)
37
47
  || JsonArgs.GetBool(args, "isolate_in_view", false)));
@@ -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
+ }
@@ -0,0 +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
+ }
@@ -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
+ }