Skip to content

Repository files navigation

DPA — Digital Passport for Artworks

This repository is a working monorepo, not a plan. It consolidates five separate prototypes from the AABC × SDA Bocconi research programme into one pipeline, one passport, one score, and one disclosure model. The code runs, the tests pass, CI is green, and the site is live.


58 is worse than 28

This is the intellectual core of the whole system, so it comes first.

Two objects in the demo score low. The Bura askos scores 58 and is structurally uncovered. The Getty Bronze scores 28 and is well covered. The lower number is the safer object.

  • 28, well covered means the registers that ought to hold this object were able to look, and looked, and found six identifying hits. The number is about the object: records exist and they are consistent.
  • 58, structurally uncovered means no register could ever have named this object. The Bura askos came out of an unrecorded archaeological site; there is no accession, no theft report, nothing a stolen-property register could match. The number measures our blindness, not the object.

A score shown without its coverage class is not just incomplete — it inverts. It would rank the uncoverable object as more trustworthy than the one the registers actually cleared, and it would do this most confidently for exactly the colonial and archaeological material the programme exists to serve. The assess package therefore reports three numbers that are never combined (confidence, coverage, forgery risk), and refuses to let coverage fold into the score. The /coverage route on the live site walks this argument in full.


What DPA is

DPA is the Digital Passport for Artworks: a research programme run by AABC (Ars Pro Mundo) with SDA Bocconi, defined in Annex A — Preliminary Research Framework (February 2026). Its thesis:

An artwork whose provenance is uncertain is currently invisible — it cannot be lent, studied, insured, or claimed, because surfacing it exposes its holder to legal and reputational risk. If a holder could register that object pseudonymously, prove the registration was immutable and time-stamped, and then disclose different amounts of it to different parties — public, museum, enforcement, source community — the object could re-enter scholarship without anyone first having to lose a lawsuit.

A passport here is not a certificate of authenticity and not a title deed. It is a single signed record with as many lawful views as there are roles. The whole system is built so that one record can be shown five ways without the holder having to trust anyone downstream to redact it correctly.


The live site

Eight routes, all statically exported and served from GitHub Pages:

Route What it shows
/ Overview: the pipeline, the three numbers, what is real
/demo The four demo cases, each rendered in a chosen role
/capture Stage 0: the scan-quality rubric and the live guidance loop, driven by sliders
/coverage The "58 is worse than 28" argument, worked over Bura and Getty
/disclosure One signed record shown to five roles, with a leakage proof
/exhibit A procedural 3D object with role-gated points of interest
/brand Live rebranding, the token contract, and the validator report per theme
/plan These documents, rendered from the repo markdown at build time

The four demo cases and their real generated results:

bura-askos     score 58   structurally-uncovered   human-review   0/9 identifying
benin-bronze   score 48   structurally-uncovered   human-review   0/9 identifying
getty-bronze   score 28   well-covered             human-review   6/9 identifying
schiele-wally  score 28   well-covered             human-review   3/9 identifying

All four route to human review; nothing in the demo auto-issues. That is correct behaviour, not a limitation — see ADR-002 and defect #1 in the meeting brief.


Running it locally

Requires Node 20+ and pnpm. From the repository root:

pnpm install
pnpm fixtures                              # run the pipeline over the 4 cases
pnpm build:web                             # static export to apps/web/out
pnpm --filter @dpa/api start               # request-time API on :8787
pnpm --filter @dpa/agent start bura-askos museum

pnpm fixtures runs the seven-stage pipeline over the four demo cases and writes one JSON file per (passport, role) under apps/web/public/api. It prints the four results above; the output is deterministic, so a clean checkout reproduces the published scores exactly.

The API serves the same records at request time. Role arrives in a header, and there is no authentication (this is deliberate — see the package note below):

curl -H 'X-DPA-Role: museum' localhost:8787/passports/bura-askos

To check the whole workspace:

pnpm -r typecheck        # clean
pnpm -r test             # 342 tests

The packages

Twelve packages under packages/, three apps under apps/. There is no contracts/ directory — the on-chain work was scoped but not built (see BACKLOG.md).

Package One honest sentence
schema The one passport envelope, the role model, the disclosure tiers, and the single canonical hashing/signing contract (contentHash + signableString) that every other package depends on
capture Stage 0. The scan-quality rubric, the real-time guidance loop, and the capture record — the only stage that creates record rather than querying it. Structurally forbidden from touching the provenance score
identity Image fingerprinting (SHA-256 + dHash), similarity and duplicate detection, forensic signals, and Gemini-based object identification
evidence One connector interface over eight sources plus tiered register checks — the single evidence service, so nothing else talks to a source directly
assess The three numbers that are never combined: the accumulation scorer, the coverage model, and forgery risk
issue Two issuer classes that sign the same bytes — pseudonymous wallet (secp256k1) and accredited institution (ECDSA P-256 cert chain) — plus one verifier
govern The confidentiality envelope: role-based redaction with a checked boundary, and EAS notarisation of the content hash only
lifecycle Review routing (human-review vs auto-issue), StatusList2021 revocation, amendment, and claims
payments The x402 micropayment mechanism for paid register lookups — testnet only, mechanism not economics
pipeline The composition root: runs the seven stages in order and exposes deliver(passport, role)
fixtures-build The four demo cases and the harness that runs the pipeline and writes the site's data
theme Every visual token in the solution, defined once. Institutions rebrand by configuration; the validator refuses a brand that makes the five roles or three coverage classes hard to tell apart
App One honest sentence
apps/web Next.js static export (the live site). Redacts at build time, one JSON per (passport, role), so the disclosure guarantee is physically inspectable on a static host
apps/api node:http server. Redacts at request time via the X-DPA-Role header. No auth by design — every response says so
apps/agent A CLI that runs the pipeline for one case in one role: start <case-id> [role] [--json]

Both apps/web and apps/api call the same single deliver() code path. The architecture doc explains why both exist: docs/ARCHITECTURE-v0.4.md.


Deploying it under an institution's brand

A museum, a university or a ministry running this will want it to look like theirs, and that must not require a fork. Every visual decision — colour, type scale, corner radius, content width, wordmark, nav height — is a token in packages/theme. apps/web contains no colour literals at all outside its print stylesheet; the CSS custom properties are generated from the theme definitions at build time.

NEXT_PUBLIC_THEME=atrium pnpm build:web

Three brands ship with the repository — slate (default, dark), atrium (light, museum) and campus (high-contrast, larger type for teaching and projection). Adding a fourth is one file:

export const pinacoteca = defineTheme({
  id: "pinacoteca",
  label: "Pinacoteca",
  description: "House brand for a civic picture gallery.",
  colorScheme: "light",
  identity: { wordmark: "Brera", wordmarkAccent: "·", organisation: "Pinacoteca di Brera" },
  brand: { bg: "#fbfaf7", text: "#1b1a17", accent: "#8c2f39" },
});

Anything not overridden falls back to the default, so a brand is usually three or four colours rather than a stylesheet.

Not every token is free, and that is the interesting part. Chrome — background, surfaces, rules, body text, accent — can be anything. The five role colours and three coverage colours cannot, because they carry meaning: they tell a reader which disclosure tier they are looking at and whether the score beside them can be trusted at all. A rebrand that quietly collapses enforcement and public into two similar blues has not restyled the site, it has introduced a disclosure bug wearing a stylesheet — and it will look completely fine to whoever shipped it.

So those eight tokens are themeable but validated. validateTheme checks WCAG contrast and enforces a minimum perceptual separation between role colours, measured in CIELAB rather than by comparing hex values, because hex distance is a poor proxy for whether two colours look different to a person. Every registered theme is checked by a test, so a brand that hurts legibility fails CI instead of shipping. The /brand route renders that report live.

Large-format displays

The places this is meant to run — a gallery kiosk, a Surface Hub in a seminar room, an ultrawide on a registrar's desk — are mostly not laptops. The content frame is therefore a range rather than a fixed width: contentWidth at a laptop and contentWidthWide on a very wide panel, interpolated so that every viewport in between gets a frame proportional to what it actually has. It holds roughly 74–84% of the display at any size, so nothing is ever a narrow column stranded in the middle of a 49" screen.

Widening the frame is not the same as widening the text, and the distinction is enforced. Prose is capped separately by proseMeasure, in ch so it tracks the theme's own type size; tables are capped at the width they were laid out for. The extra space is spent on arrangement — cases side by side, the pipeline in two columns, key/value pairs two-up, the 3D viewer taller, grids gaining columns — because showing more at once is useful and a 2,000px line of text is not. A test asserts that no line of text on any route, in any theme, is longer at 3440px than it is at 1280px.

Type does not grow with viewport width, deliberately. Width cannot tell you viewing distance: an ultrawide at 60cm and a wall panel across a room report the same number and want opposite type sizes. Room-scale deployments select campus, which raises the whole scale explicitly — a decision with a human behind it.


What is real and what is not

This section is load-bearing. The site is a demonstration, and it is honest about being one.

Real. The pipeline runs end to end. The scorer, the coverage model, the redaction boundary, and both signature schemes are the real implementations, exercised by 342 passing tests. The four demo scores are computed by the real scorer over real, cited sources. The redaction is not cosmetic: apps/web writes a physically separate file per role, and assertNoLeakage fails the build if an above-tier field escapes.

Not real.

  • No API keys exist in this repository. The live-evidence and live-identify paths are implemented but throw without keys, and none are committed.
  • No live register was ever contacted. Register checks run over committed fixtures. No result on the site came from INTERPOL, the Art Loss Register, or any real authority.
  • No attestation was written to any chain. Notarisation runs in mock mode. The Base Sepolia constants are real and verified, but nothing was submitted.
  • The demo signing keys are deterministic. They are seeded from a case id so the build reproduces. They must never sign anything of consequence.
  • The 3D geometry is procedural. The exhibit object is generated in code, not a photogrammetry scan of a real artefact.

The stages, in one diagram

0 CAPTURE     object → a record of it   capture: rubric, live guidance, seal
─ ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──
1 IDENTIFY    image → object            identity: fingerprint, dHash, similarity
2 INVESTIGATE object → sourced claims   evidence: 8 sources + tiered registers
3 ASSESS      claims → three numbers    assess: score, coverage, forgery risk
4 ROUTE       three numbers → decision  lifecycle: human-review vs auto-issue
5 ENVELOPE    → confidentiality tiers   schema/govern: a tier for every field
6 ISSUE       → one signed passport     issue: wallet or institution, same bytes
7 NOTARISE    → content hash on-chain   govern: EAS, hash only, mock by default

Stage 0 was added after AABC asked for it, and the rule above it is not decoration. Stages 1–7 all begin after an object has already been recorded by somebody; where no record was ever created they can measure the hole and nothing more. Capture is the only stage that creates record rather than querying it — which for a structurally-uncovered object like the Bura askos is the only new information the system can generate at all. It is not yet wired into the pipeline; see docs/CAPTURE-PROTOCOL.md.

Redaction is deliberately not a stage. It happens at the delivery boundary in deliver(passport, role), because making it a stage would imply a passport is ever "the redacted one" — when one signed record has as many lawful views as there are roles. The full account is in docs/ARCHITECTURE-v0.4.md.


The ten decisions

Every ADR is now implemented and enforced in code. Each row links to the record, which names the file that enforces it.

# Decision Enforced in
001 One passport envelope packages/schema/src/passport.ts
002 One scorer, accumulation from a floor of 30 packages/assess/src/scorer.ts
003 Coverage is mandatory and never folded into the score packages/assess/src/coverage.ts
004 ⚠️ Two issuer classes, one canonicalisation — reopened packages/issue/src/{wallet,institution}.ts
005 Confidentiality envelope, redacted at the boundary packages/govern/src/redact.ts
006 Notarise the hash only packages/govern/src/notarise.ts
007 ⚠️ VANGO stays a separate client — reopened (no code vendored)
008 One evidence service packages/evidence/src/gather.ts
009 No register check ever returns "clear" packages/schema verdict enum
010 Capture is Stage 0; scan quality never touches the score packages/capture/src/assess.ts

Two are reopened by the AABC feedback. ADR-004 has no class for a crowd-sourced contributor who records an object they do not own. ADR-007 settled VANGO's status on the premise that mobile capture was out of scope, which is no longer true. Both are carried to the meeting as open questions rather than defended — the reasoning is in docs/MEETING-BRIEF.md §7.

Consolidation sharpened two of these. ADR-002 and ADR-003 were both changed by defects that only surfaced once the packages were wired together — the score's direction and the coverage model's treatment of never-recorded objects. Those stories are in docs/DECISIONS.md and the meeting brief.


Reading order

If you are… Read
Preparing for the AABC status meeting docs/MEETING-BRIEF.md
Reviewing the decisions docs/DECISIONS.md
Understanding the code docs/ARCHITECTURE-v0.4.md
Tracing what came from where docs/INVENTORY.md
Owning one of the five upstream repos docs/MIGRATION.md
Touching anything visual — a page, a component, a theme docs/DESIGN-SYSTEM.md
Looking for what is left to do BACKLOG.md

Attribution and licence

Most of packages/ was written elsewhere, by other people, and vendored here; every vendored file carries a PROVENANCE: header, and the full mapping is in ATTRIBUTION.md. VANGO contributes no code by design (ADR-007).

Vendored code retains its original MIT licence. New code in this repository is MIT. Planning and research documents in docs/ are CC BY 4.0.

About

Digital Passport for Artworks - v0.4 consolidation plan for all AABC x SDA Bocconi provenance work to date

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages