SIGNALS Documentation
API Reference

Evidence Kind Registry

Contract for registering the observation kinds the evidence ledger accepts, their weights, and their primary-source flags.

Overview

App\Services\Evidence\EvidenceKindRegistry is the observation vocabulary of the evidence ledger. It answers one question: when something claims a value for a record's field, what is that claim worth?

Core binds it as a singleton in AppServiceProvider and seeds it from config/evidence.php, so recalibrating a weight is a config edit rather than a code change. EvidenceLedger prices every observation against this registry before it opens its write transaction: an observation whose kind is not registered rejects the whole proposal rather than entering the governance record silently priced at zero.

Two numbers describe a kind:

  • weight — how much an observation of this kind contributes to a fact's score. Scores are the sum of the distinct kinds observed. They are not averages and not probabilities; there are no confidence scores in this system.
  • primary — whether the kind is a primary source, strong enough to carry a fact on its own. A fact with no primary observation can never be auto-verified however high its arithmetic score climbs.

Registration is calibration, not history. Every observation the ledger writes snapshots the weight_at_capture and is_primary_at_capture it was priced under, so changing a weight here only affects observations captured afterwards. A fact that has already been scored, verified, or dismissed cannot be rewritten by a later recalibration — or by uninstalling the plugin that declared the kind.

This metadata registry does not publish a separate registered-value interface. Its current framework contract is the typed public surface on this page plus the documented registration shape and runtime behavior below.

Public surface

Method Purpose
register() Register or replace one kind definition
registerMany() Seed many kinds from a config/evidence.php-shaped map
has() Non-throwing existence check for a kind key
get() Return one kind definition, throwing when it is unknown
all() Return the full keyed definition map
keys() Return the flat list of registered kind keys

Accepted registration shape

register() takes an App\Services\Evidence\EvidenceKindDefinition — a readonly value object of key, weight, isPrimary, and an optional description.

registerMany() takes the config-shaped map used by config/evidence.php:

[
    'import.exact_name' => ['weight' => 100, 'primary' => true],
    'import.fuzzy_similarity' => ['weight' => 55, 'primary' => false],
]

The registry performs no runtime validation of the values themselves: weights are stored unchanged, and the shape is enforced by PHPDoc and static analysis rather than by a runtime guard. The scoring bands that give the numbers meaning — verified_threshold and suggested_threshold — live alongside the kinds in config/evidence.php.

Identity, collisions, and namespacing

Identity is the kind key. The registry is a keyed map, so register() replaces an existing definition rather than throwing; the last registration for a key wins. That is deliberate — recalibration is the normal case, and the ledger's captured snapshots make replacement harmless to existing facts.

Because replacement is silent, the guard against a plugin repricing core evidence lives at the registration boundary rather than in the registry:

  • plugin-scoped registrations must carry the vendor prefix, so a package publishes into its own corner of the vocabulary
  • the prefixes core ships kinds under — import. and dedup. — are reserved outright, so no package name can be chosen to reach them

Both rails reject with App\Sdk\PluginRegistrarException at plugin boot, before any observation of the offending kind can be written.

Current core registrations

Key Weight Primary Current meaning
import.exact_name 100 Yes Byte-equal name match during import matching
import.normalised_name 90 Yes Case/punctuation-folded name match
import.label_match 80 Yes Match on a labelled identifier
import.alias_match 70 Yes Match through a recorded alias
import.fuzzy_similarity 55 No Fuzzy resemblance only — never verifies alone
dedup.exact_field_match 100 Yes Byte-equal on every configured duplicate field
dedup.normalised_match 90 Yes Folded equality — suggests alone, verifies when corroborated
dedup.trigram_similarity 55 No Trigram resemblance only — always a question for a human

Lookup and failure behavior

  • has($key) is the non-throwing existence check
  • get($key) throws App\Exceptions\Evidence\UnknownObservationKindException, naming the unknown key and listing the registered vocabulary, so the failure reads as a mistake rather than a mystery
  • keys() and all() preserve registration order — config-seeded core kinds first, runtime registrations after

The registry never writes to the database and never scores anything itself. EvidenceLedger owns proposal, scoring, banding, and the audit trail.

Worked example

Seeding the registry is exactly what the container binding does:

$registry = new EvidenceKindRegistry;

$registry->registerMany(config('evidence.kinds'));

$registry->register(new EvidenceKindDefinition(
    key: 'acme.companies_house_lookup',
    weight: 100,
    isPrimary: true,
    description: 'Matched against the Companies House register.',
));

With that registration in place:

  • has('acme.companies_house_lookup') returns true
  • get('acme.companies_house_lookup')->weight returns 100
  • an ObservationInput citing that kind is accepted by EvidenceLedger::propose() and captured with weight_at_capture: 100
  • lowering the weight to 40 later changes what the next observation is worth, and nothing about the observation already captured