Review

A link that rests on little evidence can be a wrong match. Tilores can put such links into a review queue, where a person or an AI agent decides them. A reviewer can also put an entity into the queue by hand.

Enable the Review

The review needs three things.

  1. In deployment/tilores/main.tf, enable the review resources in the tilores module and deploy:

    module "tilores" {
      # other parameters
      enable_review = true
    }
  2. In the rule configuration, add a review block:

    "review": {
      "maxWeakRules": 1,
      "strongRuleIDs": ["R2_EMAIL"],
      "joinStrategy": "defer"
    }
    Option Meaning
    maxWeakRules A link is weak if at most this many rules matched. The default 0 makes no link weak.
    strongRuleIDs Rules that are enough on their own. A link that matched one of them is never weak.
    joinStrategy merge (default): join the records first and review the join afterwards. defer: keep the records apart until the review decides.
  3. Give the client the privileges tilores/query.review (read the queue and the decisions) and tilores/mutation.review (create, claim, release and resolve cases). See Authorization.

Review Cases

A review case lists pairs of records, called links, and the rules that matched them.

Case Raised by Asks
Detected a weak link with the merge strategy The records are joined. Does the join stand?
Deferred a weak link with the defer strategy The records are apart. The candidate has deferred: true. Should they be joined?
One entity createReviewCase(input: {entityID: ...}) Is this entity a wrong merge? The case has no links.
Two entities createReviewCase(input: {entityID: ..., otherEntityID: ...}) Are these two entities, which no rule connects, the same?

Asking twice for the same entities returns the case that is already waiting.

Decide a Case

  1. List the queue with reviewCases, oldest case first.

  2. Claim a case with claimReviewCase. The claim locks every entity that the records of the case are in now, and returns a lock. If stale is true, the case asks nothing any more and nothing is locked.

  3. Resolve the case with resolveReviewCase, or give it back with releaseReviewCase.

mutation {
  claimReviewCase(input: {
    id: "4f1c..."
    actor: {kind: HUMAN, id: "jane@example.com", name: "Jane"}
  }) {
    lock
    stale
    case { candidates { links { a b rules { id } } } }
  }
}
mutation {
  resolveReviewCase(input: {
    id: "4f1c..."
    lock: "review:..."
    actor: {kind: HUMAN, id: "jane@example.com", name: "Jane"}
    verdicts: [{a: "customer-2:0", b: "customer-1:0", keep: false}]
    reason: "different date of birth"
  }) {
    triggered
  }
}

Every link of the case needs a verdict. A verdict can also name any other two records of the locked entities, for example to remove a wrong edge next to the flagged one, or to create an edge that no rule found.

Verdict An edge connects the two records Effect
keep: false yes The edge is removed. Records that are no longer connected split off.
keep: false no Recorded only.
keep: true yes Recorded only.
keep: true no An edge is added: with the rules of the link, or as a MANUAL edge for any other pair. Two entities are joined, unless the consistency rules keep them apart.

The change is applied asynchronously. triggered tells whether there was a change to apply. A MANUAL edge stays when its records are updated, and is removed when one of its records is deleted or a later review rejects the pair.

Automatic Decisions

Tilores rechecks every case that an ingest raises. It resolves the case if each link has an answer:

  • The newest past verdict for the same pair of records answers the link, whatever rules matched. So a decision still holds when a decided record is submitted again.

  • For a deferred link without a past verdict: if other links joined the two records in the meantime, the link is kept. If one of the records is gone, the link is rejected. These two verdicts come from the data, so they do not answer later cases.

Such a decision has the actor kind PROCESS and the name "Automatic recheck". A case with a link that has no answer waits for a person, and so does every case created by hand.

History

reviewDecisions returns the decisions about a set of records, newest first. Ask with the records of an entity, because entity IDs change when entities merge or split.

query {
  reviewDecisions(input: {recordIDs: ["customer-1", "customer-2"]}) {
    decisions { id decidedAt actor { kind name } reason verdicts { a b keep } }
  }
}

Errors

Error When What to do
tilores.entity_busy Claim: another operation holds an entity of the case, for example an ingest or another reviewer. Try again a moment later.
tilores.review_claim_conflict Resolve: you do not hold the current claim. Claim the case again.
tilores.invalid_request The request is incomplete or names something that does not exist. Fix the request.
tilores.review_disabled Create a case while the rule configuration has no review block. Enable the review.