def _update_single_note_status_history(mergedNote, currentTimeMillis, newScoredNotesSuffix="_sn"):
  """For a single row (representing one note) in note status history, which contains fields
  merged between the old note status history and the note's current status, compute the new
  note's status history.

  Args:
      mergedNote: row of a pd.DataFrame, result of joining noteStatusHistory with scoredNotes
      currentTimeMillis: the timestamp to use for the note's new status in note_status_history
      newScoredNotesSuffix (str, optional): Defaults to "_sn". Merge suffix to distinguish
        fields from previous noteStatusHistory vs new note status.

  Returns:
      row of pd.DataFrame
  """
  # This TS will be set by run_combine_scoring_outputs.
  mergedNote[c.timestampMinuteOfFinalScoringOutput] = np.nan

  if mergedNote[c.finalRatingStatusKey] != mergedNote[c.currentLabelKey]:
    # Changed status vs. previous run:
    mergedNote[c.timestampMillisOfMostRecentStatusChangeKey] = currentTimeMillis
  else:
    # No change in status vs. previous run
    # If the note has not changed status (since the launch of this feature on 2024/07/02),
    # then the timestamp of the most recent status change should be set to -1 by default.
    if c.timestampMillisOfMostRecentStatusChangeKey not in mergedNote.index:
      mergedNote[c.timestampMillisOfMostRecentStatusChangeKey] = -1
    elif pd.isna(mergedNote[c.timestampMillisOfMostRecentStatusChangeKey]):
      mergedNote[c.timestampMillisOfMostRecentStatusChangeKey] = -1

  # Update the current status in accordance with this scoring run.
  assert not pd.isna(mergedNote[c.finalRatingStatusKey])
  mergedNote[c.currentLabelKey] = mergedNote[c.finalRatingStatusKey]
  mergedNote[c.currentCoreStatusKey] = mergedNote[c.coreRatingStatusKey]
  mergedNote[c.currentExpansionStatusKey] = mergedNote[c.expansionRatingStatusKey]
  mergedNote[c.currentGroupStatusKey] = mergedNote[c.groupRatingStatusKey]
  mergedNote[c.currentDecidedByKey] = mergedNote[c.decidedByKey]
  mergedNote[c.currentModelingGroupKey] = mergedNote[c.modelingGroupKey]
  mergedNote[c.timestampMillisOfNoteCurrentLabelKey] = currentTimeMillis
  mergedNote[c.currentMultiGroupStatusKey] = mergedNote[c.multiGroupRatingStatusKey]
  mergedNote[c.currentModelingMultiGroupKey] = mergedNote[c.modelingMultiGroupKey]

  # Lock notes which are (1) not already locked, (2) old enough to lock and (3)
  # were decided by logic which has global display impact.  Criteria (3) guarantees
  # that any CRH note which is shown to all users will have the status locked, but
  # also means that if a note is scored as NMR by all trusted models and CRH by an
  # experimental model we will avoid locking the note to NMR even though the note was
  # in scope for a trusted model and scored as NMR at the time of status locking.
  # The note will continue to be CRH only as long as an experimental model continues
  # scoring the note as CRH (or a core model scores the note as CRH).  If at any point
  # both experimental models and core models score the note as NMR, then the note will
  # lock to NMR.

  # Check whether the note has already been locked.
  notAlreadyLocked = pd.isna(mergedNote[c.lockedStatusKey])
  # Check whether the note is old enough to be eligible for locking.
  lockEligible = c.noteLockMillis < (currentTimeMillis - mergedNote[c.createdAtMillisKey])
  # Check whether the note was decided by a rule which displays globally
  trustedRule = mergedNote[c.decidedByKey] in {
    rule.get_name() for rule in RuleID if rule.value.lockingEnabled
  }

  if notAlreadyLocked and lockEligible and trustedRule:
    mergedNote[c.lockedStatusKey] = mergedNote[c.finalRatingStatusKey]
    mergedNote[c.timestampMillisOfStatusLockKey] = currentTimeMillis

  if pd.notna(mergedNote[c.lockedStatusKey]):
    # Clear timestampMillisOfNmrDueToMinStableCrhTimeKey if the note is locked.
    mergedNote[c.timestampMillisOfNmrDueToMinStableCrhTimeKey] = -1
  else:
    # If note is unlocked, allow updates to the stabilization timestamp and first
    # stabilization timestamp.
    if not pd.isna(mergedNote[c.updatedTimestampMillisOfNmrDueToMinStableCrhTimeKey]):
      mergedNote[c.timestampMillisOfNmrDueToMinStableCrhTimeKey] = mergedNote[
        c.updatedTimestampMillisOfNmrDueToMinStableCrhTimeKey
      ]
      if pd.isna(mergedNote[c.timestampMillisOfFirstNmrDueToMinStableCrhTimeKey]):
        mergedNote[c.timestampMillisOfFirstNmrDueToMinStableCrhTimeKey] = mergedNote[
          c.timestampMillisOfNmrDueToMinStableCrhTimeKey
        ]

  if pd.isna(mergedNote[c.createdAtMillisKey + newScoredNotesSuffix]):
    # note used to be scored but isn't now; just retain old info
    return mergedNote

  # If the notes created before the deleted note cutfoff. Retain
  # the old data.
  if mergedNote[c.createdAtMillisKey] < c.deletedNoteTombstonesLaunchTime:
    return mergedNote

  if np.invert(pd.isna(mergedNote[c.createdAtMillisKey])) & np.invert(
    pd.isna(mergedNote[c.createdAtMillisKey + newScoredNotesSuffix])
  ):
    assert (
      mergedNote[c.createdAtMillisKey] == mergedNote[c.createdAtMillisKey + newScoredNotesSuffix]
    )

  if pd.isna(mergedNote[c.createdAtMillisKey]):
    raise Exception("This should be impossible, we already called add new notes")

  if mergedNote[c.finalRatingStatusKey] != c.needsMoreRatings:
    if pd.isna(mergedNote[c.firstNonNMRLabelKey]):
      # first time note has a status
      mergedNote[c.firstNonNMRLabelKey] = mergedNote[c.finalRatingStatusKey]
      mergedNote[c.timestampMillisOfNoteFirstNonNMRLabelKey] = currentTimeMillis
      mergedNote[c.mostRecentNonNMRLabelKey] = mergedNote[c.finalRatingStatusKey]
      mergedNote[c.timestampMillisOfNoteMostRecentNonNMRLabelKey] = currentTimeMillis

    if mergedNote[c.finalRatingStatusKey] != mergedNote[c.mostRecentNonNMRLabelKey]:
      # NOTE: By design, this branch captures label flips between CRH and CRNH but not NMR.
      mergedNote[c.mostRecentNonNMRLabelKey] = mergedNote[c.finalRatingStatusKey]
      mergedNote[c.timestampMillisOfNoteMostRecentNonNMRLabelKey] = currentTimeMillis

  return mergedNote
For a single row (representing one note) in note status history, which contains fields
  merged between the old note status history and the note's current status, compute the new
  note's status history.

  Args:
      mergedNote: row of a pd.DataFrame, result of joining noteStatusHistory with scoredNotes
      currentTimeMillis: the timestamp to use for the note's new status in note_status_history
      newScoredNotesSuffix (str, optional): Defaults to "_sn". Merge suffix to distinguish
        fields from previous noteStatusHistory vs new note status.

  Returns:
      row of pd.DataFrame
  
sourcecode/scoring/note_status_history.py