Heratio Help Center article. Category: Plugin Reference.
ahg-authority-resolution - Technical Documentation
The ahg/authority-resolution package implements the AHG Authority Resolution Engine on the Laravel Heratio side. It turns NER-extracted name mentions into archivally-defensible authority links via an archivist-driven, evidence-based workflow with RDF-Star provenance. This is the developer reference: package layout, services, controllers, routes, ServiceProvider, schema, and dependencies.
For the user-facing perspective read "AHG Authority Resolution - User Guide" first.
Package facts
- Name:
ahg/authority-resolution - PSR-4 root:
AhgAuthorityResolution\->src/ - Path:
packages/ahg-authority-resolution/ - Licence: AGPL-3.0-or-later
- PHP: ^8.2
- Service provider:
AhgAuthorityResolution\Providers\AhgAuthorityResolutionServiceProvider - Dependencies:
ahg/core,ahg/ric,illuminate/support,illuminate/database,illuminate/http
Directory layout
packages/ahg-authority-resolution/
+- composer.json
+- database/
| +- install.sql # all 7 tables, idempotent CREATE TABLE IF NOT EXISTS
| +- seed_lookup_settings.sql # default lookup.* rows in ahg_settings
+- docs/
| +- help/ # this article and its siblings
+- resources/
| +- views/ # blade views (auth-res::)
| +- queue.blade.php
| +- review.blade.php
| +- park.blade.php
| +- create-new.blade.php
| +- settings.blade.php
| +- _candidate-card.blade.php
| +- _evidence-row.blade.php
| +- _link-different-modal.blade.php
| +- _park-modal.blade.php
| +- _reject-modal.blade.php
| +- _park-row.blade.php
| +- _park-dashboard-widget.blade.php
| +- _prefill-field.blade.php
+- routes/
| +- admin.php # 11 admin routes
+- src/
+- Console/
| +- Commands/ # 11 artisan commands
+- Http/
| +- Controllers/ # AuthorityReviewController, ParkQueueController, etc.
+- Jobs/
| +- ScoreMentionEvidenceJob.php
+- Providers/
| +- AhgAuthorityResolutionServiceProvider.php
+- Services/
+- PromoteToMentionService.php
+- ContextDerivationService.php
+- CandidateGeneratorService.php
+- EvidenceScorer.php
+- DecisionRecorder.php
+- DecisionProvenanceWriter.php
+- FieldProvenanceWriter.php
+- AuthorityCreator.php
+- ParkQueueService.php
+- NerFeedbackService.php
+- Candidate/
| +- CandidateAdapterInterface.php
| +- MysqlActorAdapter.php
| +- MysqlTermAdapter.php
| +- FusekiAgentAdapter.php
| +- FusekiPlaceAdapter.php
+- Evidence/
| +- EvaluatorInterface.php
| +- EvidenceSignal.php
| +- EvidenceDateUtil.php
| +- TemporalEvaluator.php
| +- GeographicEvaluator.php
| +- RelationalEvaluator.php
| +- RoleEvaluator.php
| +- ConflictEvaluator.php
| +- HierarchicalEvaluator.php
| +- PriorEvaluator.php
| +- CoOccurringPersonEvaluator.php
| +- PlaceConflictEvaluator.php
| +- ScaleEvaluator.php
| +- DocumentPriorService.php
+- Lookup/
+- LookupAdapterInterface.php
+- AbstractLookupAdapter.php
+- PrefillEngine.php
+- Adapters/
+- ViafAdapter.php
+- WikidataAdapter.php
+- GeoNamesAdapter.php
+- TgnAdapter.php
+- GndAdapter.php
+- IsniAdapter.php
+- SagncAdapter.php
Service catalogue
Every service is bound as a singleton in AhgAuthorityResolutionServiceProvider::register(). Resolve via app(AhgAuthorityResolution\Services\<Class>::class) or constructor injection.
Workflow orchestration
PromoteToMentionService::promoteForObject(int $objectId, array $entityTypes = ['PERSON','ORG','GPE','LOC','PLACE']): int
PromoteToMentionService::fetchSourceText(int $objectId): string
ContextDerivationService::deriveContext(int $mentionId): array
ContextDerivationService::loadTokens(): array
fetchSourceText() is also reused by NerFeedbackService to snapshot the document into ahg_ner_feedback.source_text on reject.
Candidate generation
CandidateGeneratorService::generate(int $mentionId, ?int $topN = null): array // returns inserted candidate ids
Each adapter implements CandidateAdapterInterface:
| Class | Source | Entity types |
|---|---|---|
MysqlActorAdapter |
local actor table |
PERSON, ORG, GPE |
MysqlTermAdapter |
local term table |
GPE, LOC, PLACE |
FusekiAgentAdapter |
Fuseki agents graph |
PERSON, ORG, GPE |
FusekiPlaceAdapter |
Fuseki places graph |
GPE, LOC, PLACE |
Evidence scoring
EvidenceScorer::scoreCandidate(int $candidateId): ?array
EvidenceScorer::scoreAllForMention(int $mentionId): array // {scored_count: int}
Each evaluator implements EvaluatorInterface:
public function dimension(): string;
public function supports(string $entityType): bool;
public function evaluate($mention, $context, $candidate): EvidenceSignal;
EvidenceSignal::make($signal, $data) is the factory. EvidenceSignal::MATCH, CONFLICT, SILENT, ABSENT are the four signal constants.
Decision recording
DecisionRecorder::recordLink(int $mentionId, int $candidateId, int $userId): int
DecisionRecorder::recordLinkDifferent(int $mentionId, int $candidateId, int $userId): int
DecisionRecorder::recordCreateNew(int $mentionId, int $newAuthorityId, int $userId, array $fieldDecisions): int
DecisionRecorder::recordPark(int $mentionId, int $userId, string $reason): int
DecisionRecorder::recordReject(int $mentionId, int $userId, ?string $reason = null): int
Each method:
- Inserts one row into
ahg_mention_decisionwith a frozen JSON snapshot of evidence and visible candidates. - Updates
ahg_mention.state. - For link / link_different: back-updates
ahg_ner_entity.linked_actor_id. - For park: writes
ahg_mention_park. - For reject: calls
NerFeedbackService::captureFromRejection()inside a try / catch (best-effort - failure never blocks the audit row). - Fires
DecisionProvenanceWriter::write()to push RDF-Star triples into Fuseki. Failures are logged;auth-res:write-provenancebackfills.
Provenance writers
DecisionProvenanceWriter::write(int $decisionId): bool
FieldProvenanceWriter::writeForNewAuthority(int $decisionId, int $authorityId, array $fieldDecisions): int
DecisionProvenanceWriter reads authority_resolution.decisions_graph_uri from ahg_settings and has a DEFAULT_GRAPH_URI constant.
FieldProvenanceWriter is called by AuthorityCreator only on create_new.
Authority creation
AuthorityCreator::createForMention(int $mentionId, array $fieldDecisions, int $userId): int // new authority id
Inserts the new actor (or term) row via the Qubit class-table-inheritance pattern, populates the i18n tables, and fires FieldProvenanceWriter. Enforces ISAAR-CPF mandatory fields via assertIsaarCpf() for PERSON / ORG.
Park queue
ParkQueueService::listFor(?int $userId, ?string $entityType, ?bool $newCandidateOnly,
?\DateTimeImmutable $sinceParked, ?string $reasonQuery,
string $sortBy, int $limit): array
ParkQueueService::countsByArchivist(): array
ParkQueueService::unparkAndRereview(int $mentionId, int $userId): array
ParkQueueService::scanForNewCandidates(): int
External lookup
PrefillEngine::prefillForMention(int $mentionId): array
PrefillEngine::search(string $query, string $entityType): array
Each adapter implements LookupAdapterInterface:
public function supports(string $entityType): bool;
public function search(string $query, string $entityType): array;
public function getName(): string;
public function getRateLimit(): int;
public function getTtlSeconds(): int;
public function getLicenseNote(): string;
AbstractLookupAdapter handles rate-limit (in-process token bucket), cache hit / miss against ahg_authority_lookup_cache, and HTTP-timeout plumbing.
NER feedback
NerFeedbackService::capture(int $mentionId, int $decisionId, string $rejectionReason, int $userId): int
NerFeedbackService::exportUnexported(string $outputDir, int $limit = 0): array // {written_path, row_count}
Controllers and routes
All routes mount under /admin/authority-resolution/ and require admin middleware.
| Verb | Path | Name | Controller method |
|---|---|---|---|
| GET | .../queue |
auth-res.queue |
AuthorityReviewController::queue |
| GET | .../review/{mention} |
auth-res.review.show |
AuthorityReviewController::show |
| GET | .../lookup |
auth-res.lookup |
AuthorityReviewController::lookup |
| POST | .../review/{mention}/link |
auth-res.review.link |
AuthorityReviewController::link |
| POST | .../review/{mention}/link-different |
auth-res.review.linkDifferent |
AuthorityReviewController::linkDifferent |
| GET | .../review/{mention}/create-new |
auth-res.review.createNew.form |
AuthorityReviewController::createNewForm |
| POST | .../review/{mention}/create-new |
auth-res.review.createNew |
AuthorityReviewController::createNew |
| POST | .../review/{mention}/park |
auth-res.review.park |
AuthorityReviewController::park |
| POST | .../review/{mention}/reject |
auth-res.review.reject |
AuthorityReviewController::reject |
| GET | .../park |
auth-res.park.index |
ParkQueueController::index |
| POST | .../park/{mention}/unpark |
auth-res.park.unpark |
ParkQueueController::unpark |
| GET | .../park/dashboard.json |
auth-res.park.dashboard |
ParkQueueController::dashboard |
| GET | .../settings/lookup |
auth-res.settings.lookup |
settings views |
Schema
Seven tables, all InnoDB + utf8mb4_unicode_ci. No FKs to base information_object / actor / term tables; this decouples the engine from base schema migrations.
| Table | Purpose | Key columns |
|---|---|---|
ahg_mention |
One workflow row per promoted NER entity | ner_entity_id UNIQUE, state |
ahg_mention_context |
Neighbourhood context packet (1:1 with mention) | mention_id UNIQUE, co_occurring_entities, nearby_dates, nearby_places, role_language_tokens (all JSON) |
ahg_mention_candidate |
Ranked candidates per mention | mention_id, rank_position, composite_score, evidence_signals, evidence_data |
ahg_mention_decision |
Immutable audit; one row per decision event | mention_id, decision_type, frozen evidence_snapshot + candidates_visible_snapshot |
ahg_mention_park |
One active row per parked mention | mention_id UNIQUE, new_candidate_available, new_candidate_check_at |
ahg_ner_feedback |
One row per reject decision | mention_id, source_text, rejection_reason, training_exported |
ahg_authority_lookup_cache |
Cache for external authority lookups | (source, entity_type, query_text) UNIQUE, JSON payload, ttl_seconds |
All state and decision_type columns are VARCHAR(N) with a comment; no MySQL ENUM per the Heratio convention. Schema lives in database/install.sql (idempotent CREATE TABLE IF NOT EXISTS).
ER summary
ahg_ner_entity --1:1-- ahg_mention --1:1-- ahg_mention_context
|
+--1:N-- ahg_mention_candidate
|
+--1:N-- ahg_mention_decision --1:0..1-- ahg_ner_feedback
|
+--1:0..1-- ahg_mention_park
(independent) ahg_authority_lookup_cache
Service provider boot sequence
AhgAuthorityResolutionServiceProvider::register() binds every service as a singleton (workflow orchestration, candidate adapters, ten evaluators, DocumentPriorService, EvidenceScorer, DecisionRecorder, both provenance writers, AuthorityCreator, ParkQueueService, NerFeedbackService, all seven lookup adapters, PrefillEngine).
AhgAuthorityResolutionServiceProvider::boot():
- Loads routes from
routes/admin.php. - Loads views from
resources/views/under theauth-res::namespace. - Registers the 11 artisan commands.
- Probes for
ahg_mentionviaSchema::hasTable()inside an outer try / catch (CI guard - the schema probe must not crash a fresh install before the install SQL has run; seereference_ci_schema_hastable.md). - Idempotent install: if
ahg_mentionis missing, runs the install SQL. Subsequent boots skip. - Auto-seeds the lookup-settings rows from
database/seed_lookup_settings.sqlwhen the table is missing the expected keys.
Wiring summary
PromoteToMentionService
\-> ContextDerivationService
CandidateGeneratorService
\-> [MysqlActorAdapter, MysqlTermAdapter, FusekiAgentAdapter, FusekiPlaceAdapter]
EvidenceScorer
\-> [Temporal, Geographic, Relational, Role, Conflict,
Hierarchical, Prior, CoOccurringPerson, PlaceConflict, Scale]
\-> DocumentPriorService
ParkQueueService
\-> CandidateGeneratorService
\-> EvidenceScorer
DecisionRecorder
\-> DecisionProvenanceWriter
\-> NerFeedbackService (on reject only)
AuthorityCreator
\-> PrefillEngine
\-> FieldProvenanceWriter
PrefillEngine
\-> [ViafAdapter, WikidataAdapter, GeoNamesAdapter, TgnAdapter,
GndAdapter, IsniAdapter, SagncAdapter]
Settings keys
All settings live in ahg_settings.
Workflow / scoring:
authority_resolution.candidate_top_n(int, default 5)authority_resolution.role_language_tokens(JSON array)authority_resolution.prior.<fonds_id>(JSON, 24-hour TTL cache; written byDocumentPriorService)
Provenance:
authority_resolution.decisions_graph_uri(string, defaulturn:heratio:auth-res:graph:decisions)authority_resolution.field_provenance_graph_uri(string, defaulturn:heratio:auth-res:graph:field-provenance)
External lookup (per source <src> in {viaf, wikidata, geonames, tgn, gnd, isni, sagnc}):
lookup.<src>.enabled(bool, default 0)lookup.<src>.rate_limit(int, calls per minute)lookup.<src>.cache_ttl(int, seconds)lookup.<src>.license_note(string)lookup.<src>.license_url(string)lookup.geonames.username(string, defaultdemo)
Cross-source:
lookup.precedence(JSON array, default["viaf","wikidata","geonames","tgn","gnd","isni","sagnc"])lookup.http_timeout(int seconds, default 8)
Tailwind 4 (not Bootstrap)
The ahg-theme-b5 package name is a historical misnomer; the Laravel Heratio CSS framework is Tailwind 4 (verified in package.json). All blade files in this package use Tailwind utilities only (bg-emerald-600, grid grid-cols-12, rounded-lg, etc.). The master layout still ships some Bootstrap-named class wrappers (container-xxl, breadcrumb) inherited from early scaffolding, but page-level content is Tailwind end to end. Modals are Tailwind-only; open / close runs through tiny inline scripts that toggle hidden / flex.
CSP
The review screen loads Leaflet from unpkg.com for the PLACE-card map preview. The host CSP allows unpkg; no policy change is needed.
Idempotency invariants
- Re-promoting a mention is a no-op (UNIQUE on
ner_entity_id). - Re-generating candidates clears and re-inserts the candidate set.
- Re-scoring a candidate overwrites its signals + data + composite score.
- A re-issued decision is allowed; the audit row is immutable, so both rows remain visible and the newest wins for the
statecolumn. - Re-parking is a no-op (UNIQUE on
ahg_mention_park.mention_id). - Provenance writes are idempotent: the Fuseki write uses
DELETE { ... } INSERT { ... } WHERE { ... }shapes keyed onahg:decision/<id>.
Known gaps and follow-ups
- NER per-mention confidence: currently a hardcoded constant (0.85) until the upstream pipeline exposes per-mention scores. Tracked in
heratio#132. The engine treatsconfidenceas advisory only; the evidence layer is the real signal. - Place coordinates: the
termtable has no lat/long columns and thepropertytable is empty for place terms, so PLACE-card map previews fall back to a world-view. Coordinate enrichment is tracked separately. - Async provenance:
DecisionRecorder::write()runs synchronously (adds ~50 ms latency on decide). A future pass can route writes through thefuseki_queue_enabledqueue. - SAGNC adapter: a stub. Returns
[]until a stable endpoint is wired.
Cross-codebase pairing
The same engine ships in two places:
- Heratio (Laravel 12) - this package.
- AtoM Heratio (Symfony 1.4) -
/usr/share/nginx/archive/atom-ahg-plugins/ahgAuthorityResolutionPlugin/.
Both share the same six tables (plus ahg_ner_feedback), the same five decision outcomes, the same ten evaluators, the same RDF-Star provenance shape, and the same seven external adapters. The UI layer differs (Tailwind 4 here, Bootstrap 5 there) but the data layer and the service contracts converge.
Related
- "AHG Authority Resolution - User Guide"
- "Authority Resolution - Review Screen Reference"
- "Authority Resolution - Park Queue"
- "Authority Resolution - Creating a New Authority Record"
- "Authority Resolution - Provenance Model"
- "Authority Resolution - Evidence Scoring"
- "Authority Resolution - CLI Commands"