> For the complete documentation index, see [llms.txt](https://platform-aec.gitbook.io/platform-aec-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://platform-aec.gitbook.io/platform-aec-docs/coordination-engine.md).

# 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 `externalId`s (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".

```jsonc
{ "kind": "derivation", "from": "<nodeId>", "to": "<nodeId>",
  "prov": "wasDerivedFrom" | "wasGeneratedBy",
  "agent": { "type": "software", "id": "trace-engine", "version": "..." },
  "activity": "acc-issue-create", "createdAt": "<iso>" }
```

It is **never rendered as a causal arrow.** Neutral styling only.

### 3b. `claim` - a human assertion. A signed statement.

```jsonc
{ "id": "<claimId>", "kind": "claim",
  "from": "<nodeId>", "to": "<nodeId>",            // ALWAYS pairwise, never a set
  "relation": "causes" | "contributes-to" | "recurrence-of" | "relates-to" | "duplicate-of" | "supersedes",
  "directional": true,                              // relates-to / duplicate-of are symmetric
  "status": "suggested" | "confirmed" | "rejected" | "retracted",
  "assertedBy":  { "type": "human"|"software", "id": "..." },
  "confirmedBy": { "type": "human", "id": "..." } | null,   // MUST be human
  "evidence": [ { "type": "...", "ref": "...", "note": "..." } ],
  "supersedesClaimId": "<claimId>" | null,          // corrections APPEND, never overwrite
  "createdAt": "<transaction time>",
  "validFrom": "<valid time>" | null                // bitemporal as two FIELDS, not a bitemporal DB
}
```

***

## 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 set `status: "confirmed"`. `confirmed` requires `confirmedBy.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`, with `supersedesClaimId`), 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-to` would 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-to` loops 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

| File                                 | Role                                                                                           |
| ------------------------------------ | ---------------------------------------------------------------------------------------------- |
| `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.json` is 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://platform-aec.gitbook.io/platform-aec-docs/coordination-engine.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
