Revit Add-in (C# host)
The C# half of the BuildPlan Revit add-in. Owns the Revit seam, the classifier, the parameter engine, and every bridge command handler.
What it owns
Everything that touches the Revit API, and everything downstream of it that does not.
🔴 The dependency points inward. The classifier and the parameter engine reference nothing but the BCL and each other; the Revit adapter references them and Revit. A boundary test asserts it every run, because the drift is silent — one convenience call and a library stops being hostable, with nothing to say so until something tries.
The message contract
Generated into bridge-messages.json from BridgeMessage.Names, and
compared with a freshly generated copy on every test run. 25 messages: 11 commands, 13 queries,
and one event the add-in pushes.
🔴 kind is declared per message, never derived from the prefix. The prefix is a threading
decision; whether something changes anything is not. A generator that conflated the two published the
dry run — which measures with every write switched off — as a command.
🔴 The prefix IS the thread decision
| prefix | where it runs | why it matters |
|---|---|---|
revit.* | inside the external-event dispatcher | it holds Revit’s main loop for as long as it takes |
buildplan.* | beside the dispatcher, on a thread-pool thread | an HTTP call inside the event would freeze Revit for the length of its timeout |
It is the only place that decision is visible at a call site, which is why a third prefix was refused when the parameter engine doubled the command set (ADR-31).
🔴 Two messages run on Revit’s main loop WITHOUT being inside the dispatcher, and the manifest has
a third value for exactly that — revit-ui-thread-inline:
revit.cancelcarries therevit.prefix because it is about Revit-side work, and is answered inline in the WebView callback, never enqueued — the dispatcher’s queue is drained by the external event, and the operation being cancelled is what is holding that event, so a queued cancel would run after the thing it was meant to stop. ⚠️ But that callback is itself on Revit’s UI thread, so a cancel can only arrive while that thread is free: it cannot interrupt a survey. Calling it “off the main loop” said the opposite of the real limitation.revit.selectionChangedis raised from Revit’s ownUIApplication.SelectionChanged— on the UI thread, outside the dispatcher.
And one message the primitives do not fit. buildplan.takeGuardNotices reads exactly like a
query and drains what it returns; calling it twice does not give the same answer. It is published
as a command, because “not idempotent” is the more load-bearing half — but the catalog has no
primitive for a destructive read, so the mismatch is carried as a note rather than smoothed away.
What it refuses to do
- 🔴 No apply. The parameter engine’s write phase was not ported; there is no command for it and a test asserts the absence over every wire name.
- No dialog from inside a handler. A modal raised in the external event blocks Revit’s UI thread and, in an agent-driven session, the whole run. Failures are returned as values.
- No dialog from the parameter guard at all — it runs inside other add-ins’ transactions, so it records and something else collects.
What it writes that is not a message
The BuildPlan export — a JSON file on disk, shaped by
schemas/revit-buildplan.schema.json in this
repository. It is not on the bridge: nothing sends it and nothing receives it, so it appears in no
message list here. It is named because a consumer of this context will meet the file and needs to
know which document defines it.
What it needs on disk
assets\ beside the assembly: the pinned taxonomy, the tuning fixtures, and the parameter engine’s
own ontology. ⚠️ This has been lost from a build’s output twice. Two commands refuse by name when
it happens, with a message saying the install lost the file and that the model is not at fault.
Notable behaviour a reader should not have to discover
- A long command keeps the synchronous response path and gets a per-request timeout, and a timeout is reported as still running — never as failure (ADR-17). A classify that outlives its timeout still succeeds, so its answer is recoverable through a query.
- Room containment is phase-correct. The obvious API is phase-blind and manufactured a 78% disagreement rate that was entirely the code.
- Unit conversion is driven by each parameter’s own spec, and an unhandled spec aborts that value and names it. A blanket factor produces a plausible-looking wrong number, which is worse than a missing one (ADR-41).