HANDOFF: webhook

Scope
Only webhook requirements are authoritative. Ignore all archived records for other projects. Updates 1–5 have been applied; superseded settings are discarded.

CURRENT choices
{
  "delete_wins": false,
  "latest_by": "timestamp",
  "scope_key": "global",
  "tie_policy": "last",
  "allow_resurrection": false,
  "amount_mode": "delta",
  "gap_policy": "ignore",
  "precision": 2,
  "inclusive_age": false,
  "max_age": 120,
  "audit_duplicate": true,
  "order": "key"
}

Interface
Implement evaluate(payload) returning:
{
  "entities": [{"tenant": ..., "entity": ..., "value": "string", "deleted": boolean}],
  "duplicates": [event_id, ...]
}
Input has integer now and events containing id, tenant, entity, seq, ts, kind ("set" or "delete"), and amount (nonnegative decimal string).
Do not mutate input.

Algorithm with current choices
1. Age filtering comes before deduplication. Discard an event when now - ts >= 120 because inclusive_age=false. No separate rejection rule for future timestamps is specified.
2. Deduplicate remaining events globally by id, keeping the first in encounter order. Append every discarded duplicate's id to duplicates because audit_duplicate=true. Preserve duplicate encounter order.
3. Group surviving events by (tenant, entity).
4. latest_by="timestamp" selects ts as the sort/tie field. Within each group, retain the last encountered event for each tied ts. Process retained events in ascending ts order. This is not a rule to keep only the single latest event.
5. Initialize each group's processed seq=0, value=0, deleted=false.
6. gap_policy="ignore": process an event only if event.seq equals the previous processed seq + 1. Skipped events do not change seq or other state. Every processed event updates seq, including a set blocked by deletion.
7. A processed delete sets deleted=true without changing value.
8. A processed set changes value only if not deleted, since allow_resurrection=false. An eligible set resets deleted=false and adds amount to value because amount_mode="delta". A set after a processed delete cannot resurrect the entity or change its value.
9. delete_wins=false: do not force final deleted=true merely because a retained delete exists. A delete skipped by the gap filter has no effect.
10. Round the final value exactly once to 2 decimal places using exact ROUND_HALF_UP. Return a fixed-point string with exactly two fractional digits, including trailing zeros. Avoid inexact floating-point arithmetic.
11. Emit each group even if every retained event was skipped; its result remains value="0.00", deleted=false.
12. Sort entities by (tenant, entity), since order="key".

Fixed-contract alternatives, relevant only if later updates change choices
- inclusive_age=true keeps events whose age equals max_age; older events are discarded.
- scope_key="tenant" deduplicates by (tenant,id); otherwise deduplication is global by id.
- audit_duplicate=false suppresses duplicate audit entries.
- latest_by="sequence" uses seq instead of ts.
- tie_policy="first" retains the first event at a tied chosen sort-field value.
- gap_policy="accept" processes events without the consecutive-sequence filter.
- allow_resurrection=true permits a set after deletion, resetting deleted=false.
- amount_mode="absolute" assigns amount instead of adding it.
- delete_wins=true forces final deleted=true if any event retained after tie filtering is a delete, even if the gap filter skipped that delete. It does not alter value.
- order other than "key" orders groups by their earliest retained original input index BEFORE tie filtering (after age filtering and deduplication).
- Precision controls exact fixed-point fractional digit count; precision=0 means no decimal point or fractional digits.

Status / unresolved items
All 12 configuration choices are currently resolved. No webhook-specific unresolved requirements were stated. No implementation language or validation/error-handling requirements were supplied. The next update and later questions are not available; do not anticipate them. Any future target update replaces only its named keys and leaves all other choices unchanged.