The Coordination Engine ("Weave") - data model and invariants
TRACE has two parts:
Sync - the bidirectional pipe. A Bluebeam Studio markup becomes a native ACC Issue with a 2D sheet pin; when the issue closes in Autodesk, the status is written back to Bluebeam.
Weave - the coordination layer. It threads related issues into a single coordination item and records who decided what, when, and on what evidence.
This document describes Weave's data model and the invariants the store enforces. It is the design an auditor needs; the code is in src/coordination/.
1. The problem Weave solves
One design decision spans many issues: the reviewer's markup, the model change it drives, and the problems that change creates. Those live in different systems and are named differently in each. Worse, the names are not stable: a clash re-run or a new review cycle can silently rename an issue, and every human link, note and grouping hanging off that identity is orphaned.
So the engine's hard problem is identity, not causality. Fix names first; everything else follows.
2. Identity: fingerprints, not ids
Source systems hand us volatile identifiers. Autodesk explicitly documents dbId / objectid as non-persistent across model versions. Bluebeam's fileId changes per session. Building on those guarantees churn.
Two layers, deliberately (the same pattern Git and BCF use):
fingerprint
Derived ONLY from source-stable fields. Clash = the two colliding elements' persistent externalIds (sorted) + clash type - never the run's clash-group id. QC = elementUniqueIds (persistent), never version-scoped elementIds. Review = (sessionId, fileId, page, markupName).
id
An opaque, permanent internal id (n_<uuid>), minted once, never derived.
nodeIndex
A ledger map fingerprint -> id.
Every claim, derivation and item-membership references the id, never the fingerprint.
Why opaque: if the id were derived, improving the fingerprint recipe later would rename every node - which is the exact disease being cured. The index lets the recipe change while nothing is renamed.
Fingerprint builders FAIL CLOSED. If the stable inputs are unavailable, we refuse to fingerprint rather than fingerprint volatile data. markupType is deliberately not fingerprinted: a type is mutable, and identity must not be.
3. Two disjoint link classes. A machine may write only one.
3a. derivation - a machine receipt. Immutable.
The engine created B from A. This is a fact, not an opinion. Vocabulary follows W3C PROV (wasDerivedFrom, wasGeneratedBy) - a standards body that deliberately refuses to say "caused".
It is never rendered as a causal arrow. Neutral styling only.
3b. claim - a human assertion. A signed statement.
4. THE WRITE INVARIANT (enforced in the store, not the UI)
A software agent may create a claim only with
status: "suggested". It may NEVER setstatus: "confirmed".confirmedrequiresconfirmedBy.type === "human".
Enforced in ledger.js (createClaim / confirmClaim), not in the UI - a UI-only rule is not a rule.
This is the anti-fabrication firewall. A machine-fabricated causal claim inside a record that is sold as evidence is worse than no record at all. The machine proposes. A person decides.
5. The record must be able to hold a disagreement
Status does not live on the link. If it did, one party's edit would overwrite another's and the record would silently lose the disagreement.
Two parties may file competing claims on the same pair. Both survive, both dated, both signed. One may be confirmed while the other is rejected. The UI collapses conflicting confirmed claims into one "contested" link; clicking it lists each signed claim.
Retraction is an appended claim, never a mutation. The record shows: suggested T1, confirmed by Jane T2, retracted T3. Retract is author-only - you may retract only what you asserted or confirmed (server-enforced, not just hidden in the UI).
Corrections append. Flipping or re-relating supersedes your prior claim (kept, marked
superseded, withsupersedesClaimId), so a single author never holds two contradictory positions on one pair.
A record that cannot hold a disagreement is not evidence. It is one party's story.
6. Grouping: explicit membership, never connected components
A "hub" element (a shared wall, a main duct) touches dozens of issues. If every issue links to it and groups are derived from graph connectivity, all coordination items merge into one blob.
Items hold explicit membership sets (
memberNodeIds). A node may belong to many items.A cross-item link SHARES the node into both items (both remain; the node is marked shared). It is not a destructive merge.
Unlink removes the edge only. The node stays. Split and merge are deliberate human acts.
Groups are NEVER derived from connected components. One stray
relates-towould transitively merge two unrelated items permanently, and afterwards you cannot tell which link did it. Connected components are used only to seed brand-new ungrouped nodes; a placed node is never regrouped.Causality is a DAG - causal cycles are refused at assert time. (A 2-node opposite pair is a contest, which is allowed; only 3+ loops are refused.
relates-toloops are fine.)Never transitively close. Confirmed A->B and B->C do not imply A->C. Every hop is an independently-breakable claim.
7. Nothing is deleted. Things are tombstoned.
A source item vanishing from ONE read is not a resolution - it may be a bad export or a model that failed to load. Absence increments a persistence streak; only N consecutive misses tombstone it, and a flicker resets the streak. Reappearance resolves through nodeIndex to the same id, un-tombstones, and the human's links survive intact. This generalises the sync engine's proven delete-guard.
A claim whose endpoint is tombstoned renders dangling/greyed - never auto-deleted.
8. Derived state, and what the machine may not name
Issue status is source-derived. Confirming a causal link - even by both parties - never moves issue state. (Guarded by a test.) State comes from the source systems; the ledger records decisions about it.
The machine names nothing. A coordination item spans N issues, so naming it after one member's comment is arbitrary by construction. An item is
CI-NNN+ the list of its members' comments. A name exists only when a human declares one; a machine actor is refused from naming an item (server-enforced).
9. Deliberately NOT built (each ruled out with evidence)
BCF as the internal model. It cannot express typed directional links, link status, many-to-many membership, derived state - or our 2D pin (BCF viewpoints are 3D-only). A lossy BCF exporter at the boundary is fine; conforming the internal model to BCF is not.
A bitemporal database engine (XTDB/Datomic). Two fields on an append-only log give ~90% of the evidentiary value at our scale.
Incremental view maintenance. At hundreds of items, full recompute from the log plus a cache keyed on the log head is correct.
CRDTs. Single authoritative engine + append-only log. If we ever go multi-writer: add-wins, never last-writer-wins, which would silently erase a human's causal claim.
Automatic causality. The machine may suggest; it may never assert.
10. Where the code lives
src/coordination/ledger.js
The store. Node index, claims, derivations, membership. Enforces the write invariant.
src/coordination/nodes.js
Fingerprint builders (clash / QC / review). Fail closed.
src/coordination/threads.js
Grouping + item assembly.
src/coordination/view.js
Read model for the UI (links / suggestions / provenance).
src/coordination/actions.js
The human verbs: link, relate, set-cause, unlink, confirm, dismiss, retract.
src/coordination/reconcile-hook.js
Mirrors sync state into the ledger. Try/catch wrapped - it can never throw into the sync path.
src/coordination/enabled.js
The single feature gate used by BOTH the engine mirror and the whole web surface.
src/tray/coordination.{html,js}
The node-graph UI.
Tests: scripts/test-coordination.js (run by npm test).
Weave can never break Sync. The mirror is try/catch-wrapped, writes only ledger.json, and is gated by one switch.
11. Known gaps (stated plainly)
The model plane (clash / QC) is not live. It is gated on a paid BIM Collaborate seat and on three unanswered Autodesk questions - chiefly is the clash id stable across a re-run/republish, which is the question the whole clash identity turns on. Weave currently ships the review plane only.
Recurrence (threading an unfixed problem across review cycles) depends on stable identity and a durable Sheet record. It is designed, not built.
Scale:
ledger.jsonis rewritten per tick, and view assembly is O(items x claims). This only bites at hundreds of items over months; SQLite is the documented path.
Last updated