diff method

Diffs this (current) registry against previous (P4/P5): pins whose content key changed while their owner skill persists are SUPERSEDED; pins whose owners all vanished (deleted, renamed, consent revoked) — or whose owner still exists but no longer declares the line — are DROPPED. Never silent — render the notices.

Budget drops are deliberately NOT part of the diff: a pin dropped by the previous window's budget was already reported at drop time (P6) and is not a lifecycle change — the diff baseline covers kept pins only.

Implementation

OperativePinDiff diff(SkillOperativePins previous) {
  final currentKeys = pinByKey.keys.toSet();
  final currentSkills = {
    for (final pin in pins)
      for (final p in pin.provenance) p.skillName,
  };
  final superseded = <({OperativePin previous, OperativePin current})>[];
  final droppedPins = <OperativePin>[];
  // Round 3: a successor may serve at most ONE supersede — a skill that
  // edited two pinned lines between two windows has TWO distinct
  // new-key pins, and binding both old lines to the first match named
  // the wrong "→" line in one of the notices.
  final claimedSuccessors = <String>{};
  for (final old in previous.pins) {
    if (currentKeys.contains(old.contentKey)) continue;
    if (old.provenance.any((p) => currentSkills.contains(p.skillName))) {
      // The successor must be a CURRENT pin of the same owner skill
      // carrying a NEW content key — never just the skill's first pin
      // (a multi-pin skill's unrelated surviving line must not be named
      // as the replacement, review gh-1409 round 2) and never a pin
      // already claimed by an earlier supersede in this same diff
      // (round 3).
      OperativePin? replacement;
      for (final pin in pins) {
        final isNewKey = !previous.pinByKey.containsKey(pin.contentKey) &&
            !claimedSuccessors.contains(pin.contentKey);
        final sameOwner = pin.provenance.any(
          (p) => old.provenance.any((o) => o.skillName == p.skillName),
        );
        if (isNewKey && sameOwner) {
          replacement = pin;
          claimedSuccessors.add(pin.contentKey);
          break;
        }
      }
      if (replacement != null) {
        superseded.add((previous: old, current: replacement));
      } else {
        droppedPins.add(old);
      }
    } else {
      droppedPins.add(old);
    }
  }
  return OperativePinDiff._(superseded: superseded, dropped: droppedPins);
}