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
@@ -0,0 +1,338 @@
1
+ {
2
+ "schema_version": 2,
3
+ "description": "Hand-maintained documentation index: one summary and group per public tool, plus guidance resources. Executable contracts (schemas, effects, keywords, limits, verification) are owned by code and generated into contracts.generated.json.",
4
+ "documentation_revision": "2026-09-29.1",
5
+ "groups": [
6
+ {
7
+ "id": "connection",
8
+ "title": "Connection, discovery, and local utilities"
9
+ },
10
+ {
11
+ "id": "inspection",
12
+ "title": "Model inspection and review"
13
+ },
14
+ {
15
+ "id": "spatial",
16
+ "title": "Links, coordinates, and spatial analysis"
17
+ },
18
+ {
19
+ "id": "editing",
20
+ "title": "Selection and editing"
21
+ },
22
+ {
23
+ "id": "drawings",
24
+ "title": "Drawings, schedules, and delivery"
25
+ },
26
+ {
27
+ "id": "api",
28
+ "title": "API and custom execution"
29
+ }
30
+ ],
31
+ "tools": [
32
+ {
33
+ "name": "capture_view",
34
+ "source": "bridge",
35
+ "group": "drawings",
36
+ "summary": "Capture a view as an image for visual verification."
37
+ },
38
+ {
39
+ "name": "change_element_types",
40
+ "source": "bridge",
41
+ "group": "editing",
42
+ "summary": "Change element types with partial or atomic outcomes."
43
+ },
44
+ {
45
+ "name": "create_tags",
46
+ "source": "bridge",
47
+ "group": "drawings",
48
+ "summary": "Create host element, room, space and area tags."
49
+ },
50
+ {
51
+ "name": "delete_elements",
52
+ "source": "bridge",
53
+ "group": "editing",
54
+ "summary": "Preview or perform deletion including dependent elements."
55
+ },
56
+ {
57
+ "name": "execute_csharp",
58
+ "source": "bridge",
59
+ "group": "api",
60
+ "summary": "Execute custom synchronous C# in Revit."
61
+ },
62
+ {
63
+ "name": "export_documents",
64
+ "source": "bridge",
65
+ "group": "drawings",
66
+ "summary": "Export sheets, views or the model to PDF, DWG, PNG or IFC."
67
+ },
68
+ {
69
+ "name": "find_revit_tools",
70
+ "source": "native",
71
+ "group": "connection",
72
+ "summary": "Discover tools, activate capabilities, or find packaged documentation."
73
+ },
74
+ {
75
+ "name": "get_element_details",
76
+ "source": "bridge",
77
+ "group": "inspection",
78
+ "summary": "Inspect element parameters, geometry bounds and identity."
79
+ },
80
+ {
81
+ "name": "get_element_relationships",
82
+ "source": "bridge",
83
+ "group": "inspection",
84
+ "summary": "Inspect hosts, joined elements, members and dependents."
85
+ },
86
+ {
87
+ "name": "get_element_types",
88
+ "source": "bridge",
89
+ "group": "inspection",
90
+ "summary": "Discover available element types and family symbols."
91
+ },
92
+ {
93
+ "name": "get_elements",
94
+ "source": "bridge",
95
+ "group": "inspection",
96
+ "summary": "Query, count and filter elements and project parameter values."
97
+ },
98
+ {
99
+ "name": "get_linked_elements",
100
+ "source": "bridge",
101
+ "group": "spatial",
102
+ "summary": "Query elements inside a selected linked document."
103
+ },
104
+ {
105
+ "name": "get_linked_models",
106
+ "source": "bridge",
107
+ "group": "spatial",
108
+ "summary": "Discover loaded and unloaded Revit link instances."
109
+ },
110
+ {
111
+ "name": "get_model_coordinates",
112
+ "source": "bridge",
113
+ "group": "spatial",
114
+ "summary": "Read base points, project locations and shared-coordinate mappings."
115
+ },
116
+ {
117
+ "name": "get_model_health",
118
+ "source": "bridge",
119
+ "group": "inspection",
120
+ "summary": "Inspect model warnings and health information."
121
+ },
122
+ {
123
+ "name": "get_model_overview",
124
+ "source": "bridge",
125
+ "group": "inspection",
126
+ "summary": "Inspect project identity, units, levels and category counts."
127
+ },
128
+ {
129
+ "name": "get_revit_operation",
130
+ "source": "native",
131
+ "group": "connection",
132
+ "summary": "Inspect an original operation receipt after uncertain results."
133
+ },
134
+ {
135
+ "name": "get_schedule_fields",
136
+ "source": "bridge",
137
+ "group": "drawings",
138
+ "summary": "Discover eligible parameter and field-type pairs."
139
+ },
140
+ {
141
+ "name": "get_schedules",
142
+ "source": "bridge",
143
+ "group": "drawings",
144
+ "summary": "Inspect schedule fields, sorting, filters and table cells."
145
+ },
146
+ {
147
+ "name": "manage_element_sets",
148
+ "source": "bridge",
149
+ "group": "inspection",
150
+ "summary": "Retain query membership and reread current member values."
151
+ },
152
+ {
153
+ "name": "manage_revit_instances",
154
+ "source": "native",
155
+ "group": "connection",
156
+ "summary": "List and select local Revit bridge sessions."
157
+ },
158
+ {
159
+ "name": "manage_revit_scripts",
160
+ "source": "native",
161
+ "group": "connection",
162
+ "summary": "Save, inspect and run exact versions of reusable scripts."
163
+ },
164
+ {
165
+ "name": "manage_schedules",
166
+ "source": "bridge",
167
+ "group": "drawings",
168
+ "summary": "Create and configure schedules, fields, filters and sorting."
169
+ },
170
+ {
171
+ "name": "manage_selection",
172
+ "source": "bridge",
173
+ "group": "editing",
174
+ "summary": "Read or change selection, zoom and temporary isolation."
175
+ },
176
+ {
177
+ "name": "manage_sheet_placements",
178
+ "source": "bridge",
179
+ "group": "drawings",
180
+ "summary": "List, place and move viewports and schedules on sheets."
181
+ },
182
+ {
183
+ "name": "manage_sheets",
184
+ "source": "bridge",
185
+ "group": "drawings",
186
+ "summary": "Create, rename and renumber sheets."
187
+ },
188
+ {
189
+ "name": "manage_views",
190
+ "source": "bridge",
191
+ "group": "drawings",
192
+ "summary": "Create, duplicate and update plans, sections and 3D views."
193
+ },
194
+ {
195
+ "name": "measure_geometry",
196
+ "source": "bridge",
197
+ "group": "spatial",
198
+ "summary": "Measure point distance or approximate element bounding-box gaps."
199
+ },
200
+ {
201
+ "name": "open_view",
202
+ "source": "bridge",
203
+ "group": "editing",
204
+ "summary": "Activate a Revit view or sheet."
205
+ },
206
+ {
207
+ "name": "ping",
208
+ "source": "native",
209
+ "group": "connection",
210
+ "summary": "Check Revit bridge availability and versions."
211
+ },
212
+ {
213
+ "name": "query_spatial_elements",
214
+ "source": "bridge",
215
+ "group": "spatial",
216
+ "summary": "Query elements by bounding-box intersection or containment."
217
+ },
218
+ {
219
+ "name": "read_revit_result",
220
+ "source": "native",
221
+ "group": "connection",
222
+ "summary": "Read saved large results in bounded fragments."
223
+ },
224
+ {
225
+ "name": "search_api_docs",
226
+ "source": "bridge",
227
+ "group": "api",
228
+ "summary": "Search installed Revit API classes, methods, signatures and enums."
229
+ },
230
+ {
231
+ "name": "set_parameters",
232
+ "source": "bridge",
233
+ "group": "editing",
234
+ "summary": "Edit or preview parameter values with optional atomic rollback."
235
+ },
236
+ {
237
+ "name": "summarize_elements",
238
+ "source": "bridge",
239
+ "group": "inspection",
240
+ "summary": "Count a whole element query grouped by type, category, level or parameter."
241
+ },
242
+ {
243
+ "name": "transform_elements",
244
+ "source": "bridge",
245
+ "group": "editing",
246
+ "summary": "Move, copy or rotate elements with preview rollback."
247
+ }
248
+ ],
249
+ "guidance": [
250
+ {
251
+ "name": "execution-rules",
252
+ "kind": "reference",
253
+ "path": "references/execution-rules.md",
254
+ "summary": "Shared rules for model targeting, document identity, parameters, units and transactions.",
255
+ "keywords": [
256
+ "identity",
257
+ "document id",
258
+ "expected_document_id",
259
+ "units",
260
+ "parameters",
261
+ "transactions",
262
+ "preview",
263
+ "atomic",
264
+ "localized names"
265
+ ]
266
+ },
267
+ {
268
+ "name": "operation-recovery",
269
+ "kind": "reference",
270
+ "path": "references/operation-recovery.md",
271
+ "summary": "Recover after timeouts, cancellations and uncertain outcomes using operation receipts.",
272
+ "keywords": [
273
+ "timeout",
274
+ "retry",
275
+ "receipt",
276
+ "uncertain",
277
+ "failed",
278
+ "bridge unavailable",
279
+ "no document"
280
+ ]
281
+ },
282
+ {
283
+ "name": "visual-verification",
284
+ "kind": "reference",
285
+ "path": "references/visual-verification.md",
286
+ "summary": "Capture and inspect visible results and exported deliverables, then stop when the request is verified.",
287
+ "keywords": [
288
+ "verify",
289
+ "visual check",
290
+ "drawing check",
291
+ "image",
292
+ "overlap",
293
+ "layout",
294
+ "deliverable"
295
+ ]
296
+ },
297
+ {
298
+ "name": "tool-index",
299
+ "kind": "reference",
300
+ "path": "references/tool-index.md",
301
+ "summary": "Every public tool grouped by task, with manual links.",
302
+ "keywords": [
303
+ "tools",
304
+ "capabilities",
305
+ "tool list",
306
+ "manuals"
307
+ ]
308
+ },
309
+ {
310
+ "name": "room-documentation",
311
+ "kind": "workflow",
312
+ "path": "references/room-documentation.md",
313
+ "summary": "Room plans, sections, tags, schedules and sheets as one documentation workflow.",
314
+ "keywords": [
315
+ "rooms",
316
+ "room plan",
317
+ "room tags",
318
+ "room schedule",
319
+ "room sheets",
320
+ "room documentation"
321
+ ]
322
+ },
323
+ {
324
+ "name": "model-audit-export",
325
+ "kind": "workflow",
326
+ "path": "references/model-audit-export.md",
327
+ "summary": "Multi-step model audit, data review and requested exports.",
328
+ "keywords": [
329
+ "audit",
330
+ "review",
331
+ "model quality",
332
+ "export",
333
+ "report",
334
+ "data review"
335
+ ]
336
+ }
337
+ ]
338
+ }
@@ -40,6 +40,7 @@ namespace RevitBridge
40
40
  private readonly ToolRegistry _registry;
41
41
  private readonly string _revitVersion;
42
42
  private readonly Func<bool> _hasOpenDocument;
43
+ private readonly OperationStore _operations = new();
43
44
 
44
45
  /// <summary>How often the bridge re-checks that bridge.json still points somewhere
45
46
  /// live (see EnsureBridgeInfo). Cheap: one small file read per tick.</summary>
@@ -120,14 +121,20 @@ namespace RevitBridge
120
121
  private void WriteBridgeInfo()
121
122
  {
122
123
  Directory.CreateDirectory(BridgeInfoDirectory());
123
- File.WriteAllText(BridgeInfoPath(), JsonSerializer.Serialize(new
124
+ string json = JsonSerializer.Serialize(new
124
125
  {
125
126
  baseUrl = $"http://127.0.0.1:{_port}",
126
127
  token = _token,
127
128
  pid = Environment.ProcessId,
128
129
  revitVersion = _revitVersion,
130
+ bridgeId = _operations.Generation,
131
+ supportsOperationTracking = true,
129
132
  startedAtUtc = DateTime.UtcNow.ToString("o"),
130
- }, new JsonSerializerOptions { WriteIndented = true }));
133
+ }, new JsonSerializerOptions { WriteIndented = true });
134
+ string instances = Path.Combine(BridgeInfoDirectory(), "instances");
135
+ Directory.CreateDirectory(instances);
136
+ File.WriteAllText(Path.Combine(instances, _operations.Generation + ".json"), json);
137
+ File.WriteAllText(BridgeInfoPath(), json);
131
138
  }
132
139
 
133
140
  /// <summary>
@@ -186,8 +193,9 @@ namespace RevitBridge
186
193
  }
187
194
  }
188
195
 
189
- private static void DeleteBridgeInfoIfOwned()
196
+ private void DeleteBridgeInfoIfOwned()
190
197
  {
198
+ try { File.Delete(Path.Combine(BridgeInfoDirectory(), "instances", _operations.Generation + ".json")); } catch { }
191
199
  try
192
200
  {
193
201
  string path = BridgeInfoPath();
@@ -337,7 +345,10 @@ namespace RevitBridge
337
345
  private async Task<(int Status, object Response)> RouteAsync(string method, string path, Dictionary<string, string> query, string body, CancellationToken token)
338
346
  {
339
347
  if (method == "GET" && path == "/ping")
340
- return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId, addinVersion = AddinVersion });
348
+ return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId, addinVersion = AddinVersion, bridgeId = _operations.Generation, supportsOperationTracking = true });
349
+
350
+ if (method == "GET" && path.StartsWith("/operations/", StringComparison.Ordinal))
351
+ return (200, _operations.Status(WebUtility.UrlDecode(path["/operations/".Length..])));
341
352
 
342
353
  if (method == "GET" && path == "/tools")
343
354
  {
@@ -378,14 +389,6 @@ namespace RevitBridge
378
389
  if (tool is null)
379
390
  return (404, new { error = true, message = $"Unknown tool: {name}" });
380
391
 
381
- // Pre-check before enqueueing: with zero documents open Revit does not
382
- // pump ExternalEvents, so a queued call would hang instead of failing.
383
- // The in-queue NoActiveDocumentException below remains as the backstop
384
- // for a document closing between this check and execution. Tools that
385
- // never touch the Revit API (RequiresDocument = false) skip the gate.
386
- if (tool.RequiresDocument && !_hasOpenDocument())
387
- return (409, new { error = true, hasActiveDocument = false, message = "No active Revit document is open." });
388
-
389
392
  JsonElement args;
390
393
  try
391
394
  {
@@ -401,32 +404,85 @@ namespace RevitBridge
401
404
 
402
405
  int timeoutMs = ResolveTimeoutMs(query);
403
406
 
407
+ if (query.TryGetValue("operation_id", out string? operationId))
408
+ {
409
+ try
410
+ {
411
+ var (entry, isNew) = _operations.Reserve(operationId, name, args, TimeSpan.FromMilliseconds(timeoutMs));
412
+ var completion = _operations.Wait(entry);
413
+ if (isNew) _ = CompleteTrackedOperationAsync(entry, tool, args, timeoutMs);
414
+ var reply = await completion;
415
+ return (reply.Status, reply.Response);
416
+ }
417
+ catch (ArgumentException error) { return (409, new { error = true, message = error.Message, operation_id = operationId }); }
418
+ catch (InvalidOperationException error) { return (503, new { error = true, message = error.Message, operation_id = operationId }); }
419
+ }
420
+ var result = await RunToolAsync(tool, args, timeoutMs, null);
421
+ return (result.Status, result.Response);
422
+ }
423
+
424
+ private async Task CompleteTrackedOperationAsync(OperationStore.Entry entry, ITool tool, JsonElement args, int timeoutMs)
425
+ {
426
+ var reply = await RunToolAsync(tool, args, timeoutMs, entry);
427
+ _operations.Complete(entry, new OperationStore.Reply(reply.Status, reply.Response, reply.Outcome));
428
+ }
429
+
430
+ private async Task<(int Status, object Response, string? Outcome)> RunToolAsync(ITool tool, JsonElement args, int timeoutMs, OperationStore.Entry? operation)
431
+ {
432
+ string name = tool.Name;
433
+ // Cached receipts remain accessible even when the active document closes.
434
+ if (tool.RequiresDocument && !_hasOpenDocument())
435
+ return (409, new { error = true, hasActiveDocument = false, message = "No active Revit document is open." }, null);
436
+
437
+ void StartOperation()
438
+ {
439
+ if (operation != null && !_operations.TryStart(operation)) throw new TimeoutException("Operation expired before execution; no tool action was performed.");
440
+ }
441
+
404
442
  try
405
443
  {
406
444
  object? output = tool.RequiresDocument
407
445
  ? await _queue.RunAsync(uiApp =>
408
446
  {
447
+ StartOperation();
409
448
  var document = uiApp.ActiveUIDocument?.Document ?? throw new NoActiveDocumentException();
410
- RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name);
411
- return tool.Execute(args, new ToolContext(document, uiApp));
449
+ RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name, tool.Write);
450
+ // A tool declared for other document kinds is refused before it runs (inv:document-kind-declared).
451
+ RevitBridge.Tools.DocumentGuard.CheckKind(tool, document);
452
+ // Every tool that can change the model reports what it changed (model_changes),
453
+ // without tool-specific code, so future tools inherit it.
454
+ using var changes = RevitBridge.Tools.ModelChangeRecorder.For(tool, uiApp.Application, document);
455
+ var result = tool.Execute(args, new ToolContext(document, uiApp));
456
+ return changes == null ? result : changes.Attach(result);
412
457
  }, TimeSpan.FromMilliseconds(timeoutMs))
413
458
  // RequiresDocument = false tools never touch the Revit API, so they
414
459
  // run right here on the server task instead of the CommandQueue.
415
- : tool.Execute(args, new ToolContext(null, null));
460
+ : ExecuteWithoutDocument();
461
+
462
+ try { return (200, BuildToolResponse(name, output), null); }
463
+ catch (Exception error)
464
+ {
465
+ return (500, new { error = true, toolName = name, result_unavailable = true,
466
+ message = "The tool finished, but its response could not be constructed. Model or other effects may already have occurred; inspect them before retrying. " + error.Message }, "result_unavailable");
467
+ }
416
468
 
417
- return (200, BuildToolResponse(name, output));
469
+ object? ExecuteWithoutDocument()
470
+ {
471
+ StartOperation();
472
+ return tool.Execute(args, new ToolContext(null, null));
473
+ }
418
474
  }
419
475
  catch (NoActiveDocumentException ex)
420
476
  {
421
- return (409, new { error = true, hasActiveDocument = false, message = ex.Message });
477
+ return (409, new { error = true, hasActiveDocument = false, message = ex.Message }, null);
422
478
  }
423
479
  catch (ArgumentException ex)
424
480
  {
425
- return (400, new { error = true, toolName = name, message = ex.Message });
481
+ return (400, new { error = true, toolName = name, message = ex.Message }, null);
426
482
  }
427
483
  catch (Exception ex)
428
484
  {
429
- return (500, new { error = true, toolName = name, message = ex.Message });
485
+ return (500, new { error = true, toolName = name, message = ex.Message }, null);
430
486
  }
431
487
  }
432
488
 
@@ -0,0 +1,178 @@
1
+ using System.Diagnostics;
2
+ using System.Security.Cryptography;
3
+ using System.Text.Json;
4
+
5
+ namespace RevitBridge;
6
+
7
+ /// <summary>Session-local operation receipts. Expiring a result never permits the same ID to execute again.</summary>
8
+ internal sealed class OperationStore
9
+ {
10
+ internal sealed record Reply(int Status, object Response, string? Outcome = null);
11
+ internal sealed class Entry
12
+ {
13
+ internal required string Id;
14
+ internal required string Tool;
15
+ internal required string Fingerprint;
16
+ internal required long Deadline;
17
+ internal DateTimeOffset Created = DateTimeOffset.UtcNow;
18
+ internal DateTimeOffset? Started;
19
+ internal DateTimeOffset? Finished;
20
+ internal string State = "queued";
21
+ internal TaskCompletionSource<Reply>? Completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
22
+ internal Reply? SavedReply;
23
+ internal int SavedBytes;
24
+ }
25
+
26
+ private readonly object _gate = new();
27
+ private readonly Dictionary<string, Entry> _entries = new(StringComparer.Ordinal);
28
+ private readonly Queue<Entry> _results = new();
29
+ private readonly int _maxRecords;
30
+ private readonly int _maxResults;
31
+ private readonly int _maxBytes;
32
+ private int _bytes;
33
+ public string Generation { get; } = Guid.NewGuid().ToString("N");
34
+
35
+ internal OperationStore(int maxRecords = 10000, int maxResults = 128, int maxBytes = 32 * 1024 * 1024)
36
+ {
37
+ _maxRecords = maxRecords; _maxResults = maxResults; _maxBytes = maxBytes;
38
+ }
39
+
40
+ public (Entry Entry, bool IsNew) Reserve(string id, string tool, JsonElement args, TimeSpan timeout)
41
+ {
42
+ id = ValidateId(id);
43
+ string fingerprint = Fingerprint(tool, args);
44
+ lock (_gate)
45
+ {
46
+ if (_entries.TryGetValue(id, out var existing))
47
+ {
48
+ if (existing.Fingerprint != fingerprint) throw new ArgumentException("operation_id was already used with different tool arguments. No new operation was started.");
49
+ return (existing, false);
50
+ }
51
+ if (_entries.Count >= _maxRecords) throw new InvalidOperationException("Operation receipt capacity reached. Restart the bridge before starting new tracked operations; existing receipts remain queryable.");
52
+ var entry = new Entry { Id = id, Tool = tool, Fingerprint = fingerprint,
53
+ Deadline = Stopwatch.GetTimestamp() + (long)(timeout.TotalSeconds * Stopwatch.Frequency) };
54
+ _entries.Add(id, entry);
55
+ return (entry, true);
56
+ }
57
+ }
58
+
59
+ public Task<Reply> Wait(Entry entry)
60
+ {
61
+ lock (_gate)
62
+ {
63
+ ExpireQueued(entry);
64
+ return entry.Completion?.Task ?? Task.FromResult(entry.SavedReply ?? new Reply(409, new
65
+ {
66
+ error = true, operation_id = entry.Id, state = entry.State,
67
+ message = "The operation already finished, but its full result has expired. It was not repeated. Inspect current model state before deciding on a new operation.",
68
+ }));
69
+ }
70
+ }
71
+
72
+ public bool TryStart(Entry entry)
73
+ {
74
+ lock (_gate)
75
+ {
76
+ ExpireQueued(entry);
77
+ if (entry.State != "queued") return false;
78
+ entry.State = "running"; entry.Started = DateTimeOffset.UtcNow;
79
+ return true;
80
+ }
81
+ }
82
+
83
+ public void Complete(Entry entry, Reply reply)
84
+ {
85
+ lock (_gate)
86
+ {
87
+ ExpireQueued(entry);
88
+ if (entry.Completion == null) return;
89
+ Finish(entry, reply.Outcome ?? (reply.Status < 400 ? "succeeded" : "failed"), reply);
90
+ }
91
+ }
92
+
93
+ public object Status(string id)
94
+ {
95
+ var parts = id.Split(':');
96
+ if (parts.Length == 2 && Guid.TryParse(parts[1], out var uuid)) id = parts[0] + ":" + uuid.ToString("N");
97
+ lock (_gate)
98
+ {
99
+ if (!_entries.TryGetValue(id, out var entry)) return new
100
+ {
101
+ operation_id = id, bridge_id = Generation, state = "unknown",
102
+ message = "No receipt exists in this bridge session. The outcome is unknown; this does not prove that the operation never executed. Check the original bridge and model before retrying.",
103
+ };
104
+ ExpireQueued(entry);
105
+ return new
106
+ {
107
+ operation_id = id, bridge_id = Generation, tool = entry.Tool, state = entry.State,
108
+ created_at = entry.Created, started_at = entry.Started, finished_at = entry.Finished,
109
+ result_available = entry.SavedReply != null, result_http_status = entry.SavedReply?.Status,
110
+ result = entry.SavedReply?.Response,
111
+ };
112
+ }
113
+ }
114
+
115
+ private void ExpireQueued(Entry entry)
116
+ {
117
+ if (entry.State == "queued" && Stopwatch.GetTimestamp() > entry.Deadline)
118
+ Finish(entry, "expired_before_start", new Reply(408, new { error = true, operation_id = entry.Id,
119
+ message = "Operation expired before it started; no tool action was performed." }));
120
+ }
121
+
122
+ private void Finish(Entry entry, string state, Reply reply)
123
+ {
124
+ var completion = entry.Completion!;
125
+ // Keep a detached JSON value, never API objects or callbacks, in the receipt cache.
126
+ byte[] bytes;
127
+ try { bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response); }
128
+ catch (Exception error)
129
+ {
130
+ state = "result_unavailable";
131
+ reply = new Reply(500, new { error = true, message = "The tool finished but its receipt could not be serialized. Model or other effects may already have occurred. Inspect before retrying. " + error.Message });
132
+ bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response);
133
+ }
134
+ entry.State = state; entry.Finished = DateTimeOffset.UtcNow;
135
+ entry.SavedReply = new Reply(reply.Status, JsonSerializer.Deserialize<JsonElement>(bytes));
136
+ entry.SavedBytes = bytes.Length; _bytes += bytes.Length;
137
+ _results.Enqueue(entry);
138
+ entry.Completion = null;
139
+ while (_results.Count > _maxResults || _bytes > _maxBytes)
140
+ {
141
+ var old = _results.Dequeue();
142
+ _bytes -= old.SavedBytes; old.SavedBytes = 0; old.SavedReply = null;
143
+ }
144
+ completion.TrySetResult(reply);
145
+ }
146
+
147
+ private string ValidateId(string id)
148
+ {
149
+ string[] parts = id.Split(':');
150
+ if (parts.Length != 2 || parts[0] != Generation || !Guid.TryParse(parts[1], out _))
151
+ throw new ArgumentException("operation_id must contain this bridge session's bridgeId and a UUID. An ID from another bridge/restart cannot be replayed; its outcome must be checked separately.");
152
+ return parts[0] + ":" + Guid.Parse(parts[1]).ToString("N");
153
+ }
154
+
155
+ private static string Fingerprint(string tool, JsonElement args)
156
+ {
157
+ using var stream = new MemoryStream();
158
+ using (var writer = new Utf8JsonWriter(stream))
159
+ {
160
+ void Write(JsonElement element)
161
+ {
162
+ if (element.ValueKind == JsonValueKind.Object)
163
+ {
164
+ writer.WriteStartObject();
165
+ foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) { writer.WritePropertyName(property.Name); Write(property.Value); }
166
+ writer.WriteEndObject();
167
+ }
168
+ else if (element.ValueKind == JsonValueKind.Array)
169
+ {
170
+ writer.WriteStartArray(); foreach (var item in element.EnumerateArray()) Write(item); writer.WriteEndArray();
171
+ }
172
+ else element.WriteTo(writer);
173
+ }
174
+ writer.WriteStartArray(); writer.WriteStringValue(tool); Write(args); writer.WriteEndArray();
175
+ }
176
+ return Convert.ToHexString(SHA256.HashData(stream.ToArray()));
177
+ }
178
+ }