Incident attribution you can recompute: eight factors and a threshold.
The Schema Deduction Engine names the change behind an incident with arithmetic: eight factors, published weights that sum to 1.00, and a threshold. No trace ID, no model, no learned weights — every score decomposes into its factors, and the same inputs give the same score.
Written for the technical evaluator at a regulated firm, who has to explain to a Head of Risk how an attribution was reached — and for the auditor who checks it without taking our word for it.
- Factors
- 8
- Weights sum to
- 1.00
- Attribution at
- 0.80
- Scoring version
- 1.0.14
- Model
- none
Four stages, each with one output.
Each stage hands the next a result it can check, so a receipt can be traced back through every step that produced it.
- 01 · Ingest
An alert arrives
A connected source — Sentry, Datadog, Splunk On-Call or Alertmanager · Grafana — delivers an alert. What reaches attribution is its error type, the service zone it belongs to and, from every source but Datadog, its message. Déjà keeps no message from a Datadog alert, only a service check's name or a fixed label, so a Datadog alert cannot name a field. PagerDuty, AWS CloudWatch, Azure Monitor, Google Cloud Monitoring, incident.io, Rootly and FireHydrant incidents are received too, but whether they reach attribution is still being checked.
error type · message · zone
- 02 · Extract
A merge becomes fields
When a pull request merges, its changed files are read with per-language pattern parsers. Every field added, removed, renamed or retyped becomes a row the scorer can match against.
field · operation · file
- 03 · Score
Candidates are weighed
The field the incident is missing is read from its message, and each recorded change to that field in the last 30 days is scored on eight factors. With no field to read, there is nothing to score, and the receipt says so.
one score per candidate, in [0, 1]
- 04 · Seal
The outcome is signed
The highest score decides the receipt: an attribution at 0.80 or above, a low-confidence record in the band below it, otherwise a record that nothing qualified — each signed and written to an append-only table.
R1 · R1-L · R1-N
No trace ID is read at any stage. The engine's input has no field for one, and the receipts table constrains trace_id_used to false.
Read at merge time, kept as fields.
When a pull request merges, Déjà fetches its changed files and reads the diff with per-language pattern parsers — regular expressions for Python, TypeScript and JavaScript, with a generic fallback — not a syntax tree. Every field added, removed, renamed or retyped becomes a row the scorer can match an incident against.
- Never
- Your repository is never cloned. Only the changed files of merged pull requests are read, through your Git host's API.
- Per detected change
- The field name, the operation, the file path, and the matched diff line, cut to 200 characters.
- Per pull request
- Its metadata and its changed-file list, including the diff text, with secrets scrubbed before anything is written.
- On connecting GitHub
- The last two years of merged pull requests, imported the same way.
- From your error tracker
- Each event as delivered — for Sentry, including the stack trace, the tags and the user id it reports.
- Always on
- The audit log. There is no setting that turns it off.
Retention, access control and isolation are set out on the security page.
Eight factors. One deterministic score.
Not a similarity search. Each factor is a short published rule over facts the pipeline already holds; the weights sum to 1.00; and every receipt carries the scoring version that produced it.
| Factor ID | Factor | What it scores | How it scores | Weight |
|---|---|---|---|---|
| W1 | Field overlap | Whether the change touched the field the incident is missing, compared by name after normalisation. |
| 0.30 |
| W2 | Temporal proximity | How soon before the incident the change went live — its deploy time, or its merge time when no deploy is known. | e^(−ln2 · hours / 24) — a 24-hour half-life
| 0.17 |
| W3 | Blast radius | How far the change reaches: downstream services, downstream queues and files changed for the field, each saturating, then averaged. | (min(1, services/5) + min(1, queues/2) + min(1, files/3)) / 3
| 0.13 |
| W4 | Error type match | Whether the error fits the change: a missing-key error against a removed or renamed field, or a type or validation error against a changed type. |
| 0.13 |
| W5 | Author history | How many earlier attributions in this zone name the same author, counted from Déjà's own receipts. The one factor that scores a person rather than a change. |
| 0.08 |
| W6 | Zone boundary | Whether the change and the incident sit in the same service zone. |
| 0.07 |
| W7 | Producer graph distance | How many hops separate the changed producer from the failing consumer in the payload graph. |
| 0.07 |
| W8 | Schema stability | How long ago the field last changed: from its most recent recorded change — often the candidate itself — to the moment of scoring, saturating at 30 days. One value, shared by every candidate. | min(1, hours since the field last changed / 720)
| 0.05 |
| Sum | 1.00 |
Candidates are the recorded changes to the field the incident names — the same name, or its snake_case or camelCase form — from the last 30 days. The score is the weighted sum, and the top candidate decides the receipt:
- 0.90 and above
- Attributed, high confidence
- An R1 marked HIGH_CONFIDENCE_DEDUCTION — or standard, when only the merge time of the change is known and not its deploy time.
- 0.80 – 0.90
- Attributed
- An R1 marked STANDARD_DEDUCTION. In either attributed band, when the runner-up also clears 0.80 and finishes within the tie margin, the R1 says LOW_CONFIDENCE instead.
- 0.60 – 0.80
- Low confidence
- An R1-L with the highest score reached, when the top candidate changed the field where it is read, or removed or renamed it where it is declared by a service in, or feeding, the failing zone. Otherwise an R1-N.
- Below 0.60
- No attribution
- An R1-N recording what was examined and why nothing qualified.
Scoring version 1.0.14 is signed into every R1, R1-L and R1-N as scoring_version, so a receipt from March can be checked against March's rules rather than today's.
The score is computed, not inferred.
The scoring function takes no clock, no randomness and no I/O: the pipeline hands it the facts, and it returns the score with every factor that went into it. A determinism gate in CI runs it a hundred times on the same input and requires the same output.
const rawCcs = w1 * CCS_WEIGHTS.w1 + w2 * CCS_WEIGHTS.w2 + w3 * CCS_WEIGHTS.w3 + w4 * CCS_WEIGHTS.w4 + w5 * CCS_WEIGHTS.w5 + w6 * CCS_WEIGHTS.w6 + w7 * CCS_WEIGHTS.w7 + w8 * CCS_WEIGHTS.w8 let ccs = Math.max(0, Math.min(1, rawCcs))
That is the whole combination: eight factor scores, each between 0 and 1, multiplied by their weights and clamped to the unit interval. No model sits before it and nothing is learned from your data.
"The same inputs" includes the rows the pipeline reads — the author's history, the field's change history and the candidate set. Re-run over the same rows at the same moment, the engine produces the same signed R1, byte for byte, on Déjà's managed Ed25519 key. A vault that signs with its own RSA-PSS or ECDSA key gets the same signed bytes and a different signature each time.
Above the threshold, the receipt names the change.
At 0.80 or above, the pipeline signs an R1 over its canonical form — RFC 8785 JSON canonicalisation, form v5-jcs — with the vault's key, and writes it to an append-only table. Attribution, not recurrence, is the trigger: a pattern seen before gets its own receipt.
{
"dsr_version": "DSR/1.0.6",
"type": "R1",
"receipt_id": "r_5f0b2c8e3a418f6e9d278c1a7e4b90d2",
"vault_id": "a3d9e1f4-6b28-4c75-8e03-2f7b9c1d4e56",
"organization_id": "7c2e4a90-1d35-4b8f-a6e2-0f9d3b5c7a18",
"timestamp": "2026-09-14T18:22:47.000Z",
"actor": "github:5839241",
"origin": "production",
"signature": "q3N8r0bK…VvQXw==",
"signature_algorithm": "ed25519-v1",
"canonical_form_version": "v5-jcs",
"signing_key_id": "deja-managed-v1",
"prior_hash": "5d2f8a…c1a91c",
"repository": "acme/payments-api",
"pr_number": 4521,
"service_zone": "payments-checkout",
"error_class": "KeyError",
"missing_field": "customer.tax_id",
"ccs_score": "0.9400",
"matched": "true",
"confidence": "HIGH_CONFIDENCE_DEDUCTION",
"producer_graph_score": "0.6000",
"schema_stability_score": "0.0140",
"is_synthetic": false,
"is_internal_validation": false,
"is_trial": null,
"issued_at": "2026-09-14T18:22:47.000Z",
"temporal_basis": "deployed",
"anchoring_basis": "deploy",
"incident_id": "0c6e7d15-92b4-4a3f-b1e8-6d5c2f9a7b04",
"scoring_version": "1.0.14",
"signal_observation_hash": "9b41e7…d02f6a",
"attribution_margin": "0.1100"
}What the signature covers
30 fields are in the signed bytes, every one present and null when unset, sorted by code point — among them the company, the receipt's own id, its type, the signing key and algorithm, and the link to the previous R1:
actoranchoring_basisattribution_margincanonical_form_versionccs_scoreconfidenceerror_classincident_idis_internal_validationis_syntheticis_trialissued_atmatchedmissing_fieldorganization_idpr_numberprior_hashproducer_graph_scorereceipt_idrepositoryschema_stability_scorescoring_versionservice_zonesignal_observation_hashsignature_algorithmsigning_key_idtemporal_basistypevault_idversion
R1s issued before 27 October 2026 were signed under v4-jcs, over 10 fields always and 14 more when set, and still verify.
All eight factor scores are kept with the receipt; the signature covers the total, W7 and W8.
A signed receipt cannot be altered without detection
The receipts table rejects updates and deletes from the application, and a change to any signed field breaks the signature the offline verifier checks. What a signature cannot rule out is a new signature by whoever holds the key — for a Déjà-managed vault, that is Déjà — which is why the copy of each bundle you export and keep is the independent check.
Signed, or hashed
GENESIS, R0, R1, R1-L, R1-N, R2, R2-F, R2-R, RG are signed with the vault's key — Ed25519 for a Déjà-managed key, RSA-PSS or ECDSA when you bring your own. R0, R2-F, R2-R receipts issued before 27 October 2026 carry a content hash instead. Every receipt type.
No tickets, no screenshots. The pipeline writes the receipt.
A merged pull request updates the payload graph, an alert from a connected source is scored against it, and the receipt is written. Nobody files anything, and nothing is added to your deploy pipeline.
For the engineer
- No tickets to file — receipts are issued by the pipeline
- No screenshots of Jira tickets or Slack threads
- No change to CI or the deploy pipeline — Déjà listens to webhooks your tools already send
- No audit-prep sprint — the evidence exists before the auditor's invite does
For the auditor
- dsr-verifier-cli — open source, Apache-2.0
- Signatures checked offline, with no network call to Déjà
- No Déjà account needed to check a receipt
- No source code access needed — the auditor checks receipts, not your codebase
- Built on the open DSR/1.0 specification
Without it, audit prep means reconstructing evidence by hand — searching Slack for incident context, screenshotting tickets, copying deployment logs into spreadsheets. The receipt exists before anyone goes looking for it: a deterministic score, not a probabilistic guess.
Checking a receipt needs no account and no call to Déjà. The signature is the contract, the published key and the open verifier are the proof, and the append-only ledger is the chain.
Try it on one repository and one alert source.
Provision a vault, connect a repository and an alert source, and the receipts accumulate: an attribution when a change clears the threshold, and a record of why not when none does — each one checkable offline with the open verifier.