service

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.

ServiceImplemented in the add-in

What it owns

Everything that touches the Revit API, and everything downstream of it that does not.

Warning

🔴 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

prefixwhere it runswhy it matters
revit.*inside the external-event dispatcherit holds Revit’s main loop for as long as it takes
buildplan.*beside the dispatcher, on a thread-pool threadan 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.cancel carries the revit. 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.selectionChanged is raised from Revit’s own UIApplication.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).