> 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.
