Skip to content
The engine

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
01 · The pipeline

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.

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

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

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

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

02 · Extraction

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.

03 · The weights

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 IDFactorWhat it scoresHow it scoresWeight
W1Field overlapWhether the change touched the field the incident is missing, compared by name after normalisation.
  • same name1.0
  • one a prefix of the other (customer_email, customer_email_address)0.7
  • close spelling (adress, address)0.7
  • known synonym (amount, sum)0.7
  • unrelated names0.0
0.30
W2Temporal proximityHow 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 hours before1.0
  • 24 hours before0.5
  • 48 hours before0.25
0.17
W3Blast radiusHow 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

  • 5 services, 2 queues, 3 files1.0
  • nothing downstream0.0
0.13
W4Error type matchWhether 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.
  • KeyError, field removed1.0
  • TypeError, type changed1.0
  • SchemaValidationError, field removed0.0
0.13
W5Author historyHow 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.
  • no earlier attribution0.0
  • one0.5
  • two or more0.8
0.08
W6Zone boundaryWhether the change and the incident sit in the same service zone.
  • same zone1.0
  • different zones0.0
  • one side unknown0.5
  • both unknown0.2
0.07
W7Producer graph distanceHow many hops separate the changed producer from the failing consumer in the payload graph.
  • 0 hops1.0
  • 1 hop0.6
  • 2 hops0.3
  • no path within 2 hops0.0
  • not placed in the graph0.5
0.07
W8Schema stabilityHow 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)

  • last changed 15 days before scoring0.5
  • last changed 30 or more days before scoring1.0
  • no recorded change to the field1.0
0.05
Sum1.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.

04 · The arithmetic

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.

packages/api/src/services/sde/ccs-math-engine.ts — verbatim
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.

05 · The receipt

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.

An R1 as a .dsr bundle carries it — example values
{
  "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.

06 · What you stop doing

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.