Association Industrial AI — AI² Reference implementation of the Digital Reasoning Thread
No system
holds the join.
A factory’s real questions are answered across ERP, PLM, MES and CAD at once. Extracts of those systems carry commercial and personal data and are almost never publishable; the public corpora that do exist contain one system each. DRT Loom generates the enterprise instead — every domain woven into a single model from one integer seed, with every relationship known by construction.
git clone https://github.com/association-industrial-ai/drt-loom
The weave draft
Weaving is notated as a grid: which threads a pass goes over, and which it goes
under. So is this generator. Six domains are the warp, eight phases are the weft,
and the eleven crossings below are
PIPELINE
— in the exact order the generator draws from its random stream. Where a pass ties
two systems together, a line joins them. Most passes draw from one system only:
what holds the six together is not the passes but the brackets above them, which
are the dependencies the generator closes before it draws anything at all.
Pull a thread out. The enterprise is rebuilt without it, and the numbers below are what the generator actually produced for that selection.
Scroll sideways →
Questions follow the domains. Each benchmark question declares the domains its reasoning path crosses, and one that crosses a domain you did not select is not emitted — an unanswerable question measures nothing.
Why the join is the hard part
Four objects. One number.
Ask the generated company what is the status of order 4711?
and there is no
single answer, because four different business objects carry that number. Retrieval
on the string 4711 returns all of them and cannot tell you which is
which — and they are all links in one causal chain.
-
PUR-4711
Purchase order
60 × bearing housing 30-1177 from Nordwerk Guss GmbH, €12,870.
erp/purchase_orders.json
-
ECO-4711
Engineering change order
Bearing housing 30-1177, revision B → C. Bearing-seat tolerance tightened after field returns showed fretting.
plm/engineering_changes.json
-
PRO-4711
Production order
12 × KDU-3-B-45-20-F, released, planned 2026-08-10 to 2026-08-30, eight routing steps.
mes/production_orders.json
-
SO-4711
Sales order
Nordhavn Marine A/S, €173,430, requested for 2026-09-12, in production.
erp/sales_orders.json
And they are the same thread
Nordwerk Guss GmbH supplies PUR-4711 delivers part 30-1177 consumed by PRO-4711 fulfils SO-4711 ordered by Nordhavn Marine A/S
Five hops across three systems. ERP holds the first and the last, PLM holds the part, MES holds the production order. No system holds the path, and no document in the corpus states it — but the generator knows it, because it built it. That is what makes an exact answer available to score against.
Output
One seed, one enterprise.
Records, documents, a knowledge graph, benchmark questions and ground truth all derive from the same model, so they cannot disagree with each other. Generation takes about 30 ms, with no network, no API keys and no runtime dependencies.
Everything a company produces, in one directory
data/generated/alpine-drive-systems/
├── config.yaml the exact configuration this was built from
├── generation.json seed, domains, counts, cross-domain edges
├── dataset.json entities and typed relations
├── gold.json only the questions these domains can answer
├── documents/ when the documents domain is selected
├── nx/ when CAD is selected
└── graph/ after `npm run graph`
The resolved configuration is written next to the output, so a company can always be rebuilt from its own artifacts — byte for byte.
Every relation is typed and carries its provenance
// dataset.json → relations[]
{
"source": "PART-10-1798",
"target": "SUP-001",
"relation": "approved_supplier",
"confidence": "EXTRACTED",
"sourceFile": "erp/approved_vendor_list.json"
}
EXTRACTED, INFERRED or AMBIGUOUS. Where
CAD carries no part number the link stays AMBIGUOUS rather than
being silently resolved, so the uncertainty is in the graph instead of hidden
behind it.
Each question carries the answer it must produce
{
"id": "Q-MH-01",
"category": "multi_hop",
"question": "Nordwerk Guss GmbH has told us bearing housing 30-1177 will slip by
three weeks. Which customer deliveries due before the end of
November are at risk, and what is the total value exposed?",
"expectedIds": ["SO-4711", "SO-4716", "SO-4720", …],
"expectedValues": { "ordersAtRisk": 16, "customersAffected": 11,
"exposureEur": 2739771.54 }
}
Eighteen questions across six categories: disambiguation, multi-hop, aggregation, absence, lookup and narrative. The narrative questions are answerable only from prose, and a knowledge graph contributes nothing to them. That is deliberate — if one architecture wins every category, the benchmark is measuring its own construction rather than the systems under test.
Verification
Ground truth is derived, not annotated.
No LLM invents an entity, a relationship or an answer. Every gold answer is computed from the finished environment by a reference oracle that shares no code with anything being evaluated, and no machine-checkable field is copied from a scripted constant.
npm run verify- Generator, gold format, citation handling and scorer are mechanically compatible on the reference seed.
npm run verify:seeds- Six seeds rebuilt from scratch: every answer re-derived and compared to gold, cross-question invariants checked, then rebuilt again to confirm the output is byte-identical.
npm run verify:domains- Ten domain configurations held to the same standard as the full one, including that relations still cross domain boundaries — one enterprise, not several datasets sharing a directory.
npm run verify:config- Configuration round-trips unchanged, dependency closure handles chains and cycles, and every kind of bad input is rejected with a message naming the actual mistake.
What this does not prove. That the answers are objectively correct. A wrong answer scored against itself still returns 1.0. The verification proves self-consistency, determinism and shape — read KNOWN-ISSUES.md before publishing any number measured here.
Extending it
A domain is code, not a key in a YAML file.
Configuration selects meaning; it does not define it. Adding scada: true
to config.yaml is a configuration error naming the typo, not a new
domain. Registering a module is what makes a domain real.
The contract
export interface DomainModule {
id: DomainId;
label: string;
dependencies?: readonly DomainId[];
contributes: readonly NodeType[];
// Keyed by phase, not a single generate(), because
// domains genuinely interleave in the random stream.
generate?: Partial<Record<Phase, (ctx) => void>>;
// Optional. A domain with nothing to assert omits it
// rather than implementing an empty method.
validate?(ctx, problems: string[]): void;
}
Registered today
| Domain | Needs | Contributes |
|---|
Dependencies are closed automatically and the addition is reported, never
applied silently. erp and plm are core: an enterprise
with no parts and no orders is not a smaller enterprise, it is an incoherent
one.
The contract, a worked PLC Engineering example, and the checklist
Build one.
npm run generate -- --name "Alpine Drive Systems" --domains erp,plm,cad --size small
Or run it with no options and it asks — company name, seed, size, and which optional domains to include. Node 20 or newer. Nothing else.