Trace every recalled component before it becomes a customer incident.
An autonomous recall blast-radius investigator for small aerospace and defence parts distributors, built end to end in Jac.
All company names, part numbers, certificates and recall bulletins in this repository are synthetic, created for this demo. No real company, part or recall is referenced.
Maria is the quality manager at a 15-person aerospace parts distributor.
A supplier bulletin lands in her inbox: part AB-442, lot A17, serials AB442-17001 through AB442-17999, manufactured in March 2026, may lose tensile strength above 180 °C. Quarantine unused inventory and notify customers.
Now she has to answer one question — who did we ship this to? — from purchase orders, shipment spreadsheets, inventory extracts, as-built assembly records and certificates that live in different files and different formats. Today that takes hours, and it fails in two expensive directions:
- Miss a customer who received an affected part, and a recalled fastener stays in service.
- Over-notify a customer who bought the same part number from a different lot, and you have burned a relationship and a week of everyone's time.
The hard part is not search. It is that the answer is a path: the recalled lot is three, five, sometimes eight relationships away from the customer, and one of those hops is often missing from the records entirely.
RecallGraph turns those records into a supply-chain graph and answers the question by traversing it — with the evidence path attached to every verdict.
A part-number search is a WHERE clause. A recall investigation is a
reachability query with three complications a table cannot express:
-
The recalled unit is not always the thing you shipped. Customer B never ordered AB-442. They ordered a landing-gear bracket assembly that has an AB-442 fastener installed inside it. Finding them means walking
lot → unit → installed component → assembly → shipment → customer— six hops, four of them across different source files. -
Absence of an edge is the finding. Customer D's assembly BOM says "AB-442 at fastener station 1" but the as-built record never captured which physical unit was installed. There is no
Referencesedge. That missing edge is precisely why they cannot be cleared — and it is invisible to a join. -
The reason has to travel with the answer. "Confirmed affected" is worthless to a quality manager without the chain that proves it. Because the walker is the traversal, the path it took is the audit trail, for free.
This is not a Jac wrapper over Python logic. Remove Jac and there is no project left.
| Layer | Where it lives | What Jac does |
|---|---|---|
| Supply-chain graph | models/*.jac |
23 node and 34 typed edge archetypes — including review/audit records — make the domain model the graph schema |
| Impact reasoning | walkers/impact_analysis.jac |
Multi-hop visit traversal, node-typed abilities, walker inheritance |
| Replacement qualification | walkers/replacements.jac |
Certificate → requirement traversal decides validity |
| Evidence & actions | walkers/action_packet.jac |
Materialises Evidence / Action nodes back onto the graph |
| Agentic orchestration | walkers/orchestrator.jac |
One walker drives eight specialist walkers, timing each stage |
| Bulletin extraction | services/extraction.jac |
by llm() structured output with sem prompts + deterministic fallback |
| API | services/api.jac |
def:pub endpoints; Jac generates the typed client RPC stubs |
| User interface | components/*.cl.jac |
The React UI is also written in Jac and compiled to JS by jac2js |
~6,900 lines of Jac plus CSS. No Python, no JavaScript, no TypeScript source files in the repository.
Jac features used deliberately, not incidentally:
- Typed edges with declared endpoints (
edge SentTo: Shipment --> Customer) so every traversal is statically typed end to end. - A shared base archetype (
SupplyNode) giving walkers a catch-all entry type for generic passes such as workspace purge. - Walker inheritance —
recall_scopeis a lookup base that reads the recall criteria off the graph; the direct and assembly trace walkers subclass it and differ only in which hops they take. - Deferred
exitabilities — ingestion materialises nodes onWorkspace entryand wires the relational edges onWorkspace exit, so linking is done by querying the graph it just built. visit ... elseget-or-create for the workspace node.disengageto stop the orchestrator after exactly one workspace.- One shared type module compiled to two runtimes —
models/views.jacdefines the wire contract for the Python server and the browser client, so renaming a field is a compile error on both sides at once.
┌───────────────────────────────────────────┐
recall │ services/extraction.jac │
bulletin ───► │ by llm() ──fallback──► parser │──► validated
(text) │ + validate_criteria() │ RecallCriteria
└───────────────────────────────────────────┘ │
▼
13 CSV files ─►┌──────────────────────────────────────────────────────────┐
│ walkers/ingestion.jac │
│ materialise nodes ──► link edges by graph query │
└──────────────────────────────────────────────────────────┘
│ supply-chain graph
▼
┌──────────────────────────────────────────────────────────┐
│ walkers/orchestrator.jac : run_full_investigation │
│ ├─ find_directly_affected_shipments (recall_scope) │
│ ├─ find_indirectly_affected_shipments (recall_scope) │
│ ├─ find_missing_evidence │
│ ├─ classify_customer_impact ──► Evidence nodes │
│ ├─ POSSIBLY_AFFECTED ──► ReviewCase │
│ ├─ find_valid_replacements │
│ └─ generate_action_packet ──► Action nodes │
└──────────────────────────────────────────────────────────┘
│ InvestigationSummary
▼
services/api.jac (def:pub) ──► components/*.cl.jac (UI)
Full detail, including the graph schema and every walker's responsibility, is in ARCHITECTURE.md.
Requirements: Python 3.11+ and Bun (Bun builds the client
bundle; jac install uses it).
# 1. install bun if you don't have it
curl -fsSL https://bun.sh/install | bash
# 2. install the Jac toolchain
python3 -m venv .venv
source .venv/bin/activate
pip install jaclang==0.16.7 jac-client byllm
# 3. install project dependencies (npm packages for the client bundle)
jac install
# 4. run it
jac start main.jacThen open http://localhost:8000.
No database, no API key, no authentication, no cloud services. The graph lives
in memory and persists to .jac/data between runs.
| Command | What it does |
|---|---|
jac start main.jac |
Start the app (API + UI) on port 8000 |
jac start main.jac --port 8801 |
Start on a different port |
jac start main.jac --dev |
Dev mode with hot reload for .cl.jac files |
jac test tests/investigation_tests.jac -v |
Run the 32 core + review tests |
jac check main.jac |
Type-check the project |
jac start main.jac --no_client |
API only, no UI bundle |
If a run ever behaves strangely after editing a node archetype, clear the
persisted graph: rm -rf .jac/data.
- Open the dashboard.
- Load demo scenario — reads the bundled bulletin and all 13 CSV files, and shows the extracted recall criteria as a validated schema.
- Run investigation — the eight traversal stages report their real node counts and timings as they complete.
- Read the customer table: status, route, shipment, part, lot, evidence completeness, recommended action.
- Click any customer row for the evidence chain, the graph path, the units examined, missing data, and the replacement recommendation.
- Open the Human review queue, select the unresolved Northpoint case, and inspect its machine result, missing evidence, preserved graph path and history.
- Choose Confirm affected, Clear / not affected, or Needs more evidence, enter a rationale, and submit. The resulting audit event remains visible even when a terminal case leaves the pending queue.
- Generate action packet — produces the drafted customer notification.
You can also paste your own bulletin into the text area before running; the
parser reads dot-leader labelled fields (Part Number ..... AB-442).
13 synthetic CSV files in demo-data/, plus recall_bulletin.txt.
| File | Rows | Contents |
|---|---|---|
suppliers.csv |
2 | AeroBolt Manufacturing, Titanhold Fasteners |
parts.csv |
5 | Part catalogue with temperature ratings |
manufacturing_lots.csv |
5 | Lots A16, A17, A18, B04, C01 |
customers.csv |
4 | Four customers with quality contacts |
purchase_orders.csv |
5 | Customer orders against parts |
inventory.csv |
11 | Physical units with lot / serial / date traceability |
shipments.csv |
5 | Outbound shipments |
shipment_items.csv |
8 | Units and assemblies inside each shipment |
assemblies.csv |
2 | ASM-88, ASM-91 |
assembly_components.csv |
4 | As-built BOM lines |
requirements.csv |
3 | AS9100D, ≥200 °C, DFARS traceability |
certificates.csv |
8 | Certificates held by parts and replacements |
replacement_parts.csv |
3 | Replacement candidates |
Ingested, this becomes 57 nodes and 102 typed edges.
Running the bundled scenario produces exactly this, every time:
| Customer | Status | Route | Why |
|---|---|---|---|
| Orbital Dynamics Aerospace (CUST-001) | 🔴 CONFIRMED AFFECTED | Direct | Shipment SHP-2101 contained INV-101 and INV-102, both lot A17. Lot, serial and manufacture date all place them inside the recall. |
| Meridian Flight Systems (CUST-002) | 🔴 CONFIRMED AFFECTED | Via assembly | Never ordered AB-442. Received assembly ASM-88 on SHP-2204, which has unit INV-104 (lot A17) installed at IC-301. Found only by walking the as-built BOM. |
| Northpoint Defense Integration (CUST-004) | 🟠 NEEDS INVESTIGATION | Direct + assembly | Two independent gaps: INV-105 shipped with no lot, serial or date recorded; and IC-304 in assembly ASM-91 names AB-442 with no as-built unit reference. Cannot be confirmed or cleared. Evidence completeness 0/6. |
| Cascade Avionics Group (CUST-003) | 🟢 SAFE | Ordered only | Did order AB-442 (PO-4495, PO-4523), which is exactly why a part-number search flags them. But every unit shipped (INV-103, INV-106, INV-110) traces to lots A16 and A18 — outside the recall on lot, serial and date. Evidence completeness 9/9. |
Also produced:
- Quarantine list — INV-108 and INV-111, both lot A17, still in stock at BIN-A-12, never shipped.
- Replacement qualification — 1 of 3 candidates approved:
- ✅ AB-450 — holds AS9100D and DFARS-7009, rated to 260 °C, approved, not itself recalled.
- ❌ TH-771 — rejected: no certificate on file satisfies AS9100D (it holds ISO9001 and DFARS-7009). Its 320 °C rating is fine.
- ❌ AB-448 — rejected: rated to 150 °C, below the required 200 °C, and it does not clear the bulletin's own 180 °C operating restriction.
- Metrics — 4 analysed · 2 confirmed · 1 needs investigation · 1 safe · 1 valid replacement.
Deterministic, and made by graph traversal only. The LLM never classifies impact — it reads the bulletin, and may narrate evidence it was handed.
Each unit of the recalled part is tested against three independent signals — lot, serial range, manufacture date — and each returns INCLUDE, EXCLUDE or UNKNOWN (unknown when the field is simply not recorded):
- INCLUDED — any signal says INCLUDE.
- EXCLUDED — at least one signal says EXCLUDE and none is UNKNOWN.
- UNKNOWN — otherwise. Missing evidence never silently clears a unit.
Rolled up per customer:
- CONFIRMED_AFFECTED — the recalled part matches, lot/serial/date confirms inclusion, and a path exists to the customer directly or through an assembly.
- POSSIBLY_AFFECTED — the part matches but required lot, serial or date evidence is missing, or an incomplete assembly relationship prevents confirmation.
- SAFE — the part does not match, or every unit that reached the customer is definitively outside the recall criteria.
The LLM is used where flexibility is useful, but consequential decisions require evidence. Missing evidence leads to explicit review rather than a confident automated clearance. Human decisions are auditable and do not erase the machine's original reasoning or evidence path.
A review case is created only when the deterministic customer classifier emits
POSSIBLY_AFFECTED: no unit has an INCLUDED verdict, at least one unit/path is
UNKNOWN, and the records therefore cannot confirm or clear the customer.
Confirmed and fully excluded (SAFE) cases do not enter the queue. A confirmed
case may still have unrelated documentation gaps, but it does not need a human
override to establish impact.
The review detail exposes the investigation and recall IDs, customer, affected part, original machine status, route, deterministic explanation/recommendation, evidence completeness, named missing records, first-class evidence records and the ordered graph path. No LLM explanation is generated for review.
| Current state | Reviewer action | Resulting state |
|---|---|---|
PENDING_REVIEW |
Confirm affected | CONFIRMED_AFFECTED_BY_REVIEW |
PENDING_REVIEW |
Clear / not affected | CLEARED_BY_REVIEW |
PENDING_REVIEW |
Needs more evidence | MORE_EVIDENCE_REQUIRED |
MORE_EVIDENCE_REQUIRED |
Any of the three actions | A terminal state, or another auditable evidence-request revision |
CONFIRMED_AFFECTED_BY_REVIEW and CLEARED_BY_REVIEW are terminal. A second
decision is rejected with INVALID_TRANSITION; it cannot silently overwrite the
first. MORE_EVIDENCE_REQUIRED stays in the pending queue and may be reviewed
again, appending another revision. Every action requires a non-blank rationale.
The graph-native audit shape is:
root → ReviewLedger → ReviewCase → ReviewEvent (revision 1..n)
├──→ preserved Evidence nodes
└──→ ordered ReviewPathSnapshot nodes
ReviewCase.original_machine_status is immutable in the workflow. Each
ReviewEvent records the original machine status, prior and resulting review
states, action, rationale, reviewer ID, timestamp, evidence references and path
references. The current review state changes, but the machine result does not.
The ledger is attached directly to root, outside the operational Workspace,
so its evidence/path snapshot survives the next dataset purge and rebuild.
The typed public functions are:
| Operation | Jac endpoint |
|---|---|
| List pending (or include history) | list_reviews(investigation_id, include_completed) |
| Retrieve review detail | get_review_detail(review_id) |
| Submit validated decision | submit_review_decision(review_id, reviewer_decision, reviewer_rationale, reviewer_id) |
| Retrieve append-only history | get_review_history(review_id) |
All are available as POST /function/<name> and use Jac's standard response
envelope. Case IDs and decisions are resolved/validated server-side; a supplied
status cannot mutate the machine result.
- This is a local, minimal workflow: there is no authentication, RBAC, reviewer assignment/claim, concurrency lock, notification, or external ticketing.
- Reviewer identity is a free-text value; blank input is stored as the explicit
demo-reviewerplaceholder. - “Needs more evidence” records the request and keeps the case open, but this demo does not upload documents or automatically re-run classification when evidence arrives.
- The UI lists unresolved cases for the current investigation. Completed cases
remain retrievable through
include_completed=Trueand the history/detail endpoints, but there is no elaborate historical admin dashboard. - The included records are deterministic synthetic fixtures, not a human-subject study or a production human-review evaluation.
A replacement is valid only when all five hold:
- a
Replacesedge declares it compatible with the recalled part; - every
Requirementon the recalled part is satisfied — certifications viaHasCertificate → Satisfies, temperature against the candidate's rating; - it clears the operating restriction named in the bulletin itself;
- its approval status is
APPROVED; - it is not itself under an active recall.
Rejected candidates are returned with the specific rule they failed.
The app is fully functional with no API key. To exercise the by llm() path:
cp .env.example .env
# then set ONE of:
# NVIDIA_NIM_API_KEY=nvapi-... (checked first)
# OPENAI_API_KEY=sk-...
# ANTHROPIC_API_KEY=sk-ant-...
# GOOGLE_API_KEY=...
jac start main.jacThe "Extracted recall criteria" panel shows which provider produced the schema.
The LLM result is still put through validate_criteria(); if it fails
validation or the provider errors, RecallGraph falls back to the deterministic
parser and says so in the warnings. The demo cannot fail because an LLM is
unavailable.
jac test tests/investigation_tests.jac -v32 tests, including the original traversal suite and nine review-workflow tests, covering:
- Direct affected shipment is found (and unshipped stock is quarantined)
- A recalled part inside a shipped assembly is found via the as-built BOM
- A safe lot is not misclassified, and the false-positive customer is cleared
- Missing lot data produces POSSIBLY_AFFECTED, with named gaps
- A valid replacement is recommended
- A replacement missing a certification is rejected — for that reason only
- A replacement below the temperature requirement is rejected
- Evidence paths contain exactly the expected node refs and kinds
- Full investigation returns the expected customer counts
- Two runs produce identical results (determinism)
- Action packets carry evidence, replacement and a drafted notice
- Bulletin extraction and schema validation, including rejection cases
- Incomplete evidence enters review while sufficient evidence does not
- Rationale validation and persistence of valid decisions
- Preservation of the original machine verdict, evidence, and graph path
- Explicit transition rejection and append-only audit timestamps/history
- Pending/history queue filtering and multi-revision evidence requests
| Symptom | Fix |
|---|---|
Error: No jac.toml found |
Run jac start from the repository root. |
Bun is required for client development |
Install Bun (curl -fsSL https://bun.sh/install | bash), then jac install. |
| Port 8000 already in use | jac start main.jac --port 8801. Kill stale servers with pkill -f "jac start". |
NodeAnchor ... is not a valid reference |
Stale persisted graph after a schema edit: rm -rf .jac/data and restart. |
| UI shows a blank page after an edit | rm -rf .jac/client/compiled and restart so the client bundle is rebuilt. |
| Extraction says "No LLM API key configured" | Expected and harmless — that is the deterministic parser reporting itself. |
| Tests behave oddly on re-run | rm -rf .jac/data first; tests share a persisted root. |
main.jac entry point: server imports + cl { app }
models/
supply_chain.jac operational supply-chain node/edge archetypes
investigation.jac Investigation / Evidence / Action nodes
plus ReviewLedger / ReviewCase / ReviewEvent snapshots
views.jac wire types, compiled to BOTH server and client
walkers/
ingestion.jac ingest_recall, ingest_supply_chain_records, purge
impact_analysis.jac recall_scope base + direct/assembly traces,
classification, missing evidence, evidence path
replacements.jac find_valid_replacements
action_packet.jac generate_action_packet
orchestrator.jac run_full_investigation
review.jac queue creation, transitions, review persistence/history
services/
extraction.jac by llm() + deterministic parser + validation
dataset.jac CSV loading and column validation
graph_view.jac server-side graph layout
api.jac def:pub endpoints
runtime_env.jac offline-safe runtime defaults, .env loading
components/ the UI, written in Jac (.cl.jac -> React)
ReviewQueue.cl.jac review detail, required-rationale form, audit history
demo-data/ synthetic bulletin + 13 CSV files
tests/investigation_tests.jac
assets/global.css
See DEMO_SCRIPT.md for a four-minute presentation walkthrough.