File indexing completed on 2026-09-01 09:34:21
0001 """Remove the superseded counter-form errors component from snap history.
0002
0003 The errors component's first day recorded running counters (version 1);
0004 the interval-entries record (version 2, docs/SNAPPER_ERRORS.md) and its
0005 backfill supersede that span completely. This script removes the
0006 counter-form errors member from epicprod snap history:
0007
0008 - A snap whose only changed component was errors is deleted — its
0009 other-component state duplicates the preceding snap.
0010 - A snap that also carried other components' changes (or a baseline
0011 copy) keeps its row; the errors member is stripped from the state
0012 and the envelope vectors, and the composed state hash is cleared as
0013 no longer computed.
0014
0015 Rows written by the entries backfill (capture policy
0016 backfill-errors-v1) and version-2 entries snaps are never touched. A
0017 row referenced by a capture cursor is stripped rather than deleted.
0018 Idempotent; dry-run default.
0019
0020 Run under the venv with the swf-monitor project on the path:
0021
0022 cd <swf-monitor>/src && source <venv>/bin/activate && source ~/.env
0023 python <swf-monitor>/scripts/cleanup-errors-counter-era.py [--apply]
0024 """
0025
0026 import argparse
0027 import os
0028 import sys
0029
0030 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0031
0032 import django
0033
0034 django.setup()
0035
0036 from snapper_ai.models import CaptureCursor, SystemSnap
0037
0038 BACKFILL_POLICY = 'backfill-errors-v1'
0039
0040
0041 def main():
0042 parser = argparse.ArgumentParser(
0043 description='Remove the counter-form errors component from '
0044 'epicprod snap history.')
0045 parser.add_argument('--apply', action='store_true',
0046 help='write the changes (dry run without)')
0047 args = parser.parse_args()
0048
0049 protected = set(
0050 CaptureCursor.objects
0051 .filter(latest_snap__isnull=False)
0052 .values_list('latest_snap_id', flat=True))
0053
0054 rows = (SystemSnap.objects
0055 .filter(scope='epicprod',
0056 state__components__has_key='errors')
0057 .exclude(capture_policy=BACKFILL_POLICY)
0058 .order_by('snap_time'))
0059
0060 deletions = []
0061 strips = []
0062 for snap in rows.iterator():
0063 errors = (snap.state.get('components') or {}).get('errors') or {}
0064 if 'entries' in (errors.get('data') or {}):
0065 continue
0066 changed = list(snap.changed_components or [])
0067 if changed == ['errors'] and snap.id not in protected:
0068 deletions.append(snap)
0069 else:
0070 strips.append(snap)
0071
0072 print(f'counter-form errors members found: '
0073 f'{len(deletions) + len(strips)}')
0074 print(f' rows to delete (errors-only change): {len(deletions)}')
0075 print(f' rows to strip (other components kept): {len(strips)}')
0076 for snap in (deletions + strips)[:3]:
0077 print(f' e.g. {snap.snap_time.isoformat()} '
0078 f'changed={snap.changed_components}')
0079
0080 if not args.apply:
0081 print('\ndry run — nothing written; --apply performs the cleanup')
0082 return 0
0083
0084 deleted = 0
0085 for snap in deletions:
0086 snap.delete()
0087 deleted += 1
0088 stripped = 0
0089 for snap in strips:
0090 snap.state.get('components', {}).pop('errors', None)
0091 snap.changed_components = [
0092 name for name in (snap.changed_components or [])
0093 if name != 'errors']
0094 for field in ('component_revisions', 'registration_versions',
0095 'component_hashes'):
0096 mapping = getattr(snap, field) or {}
0097 mapping.pop('errors', None)
0098 setattr(snap, field, mapping)
0099 snap.state_hash = ''
0100 snap.save(update_fields=[
0101 'state', 'changed_components', 'component_revisions',
0102 'registration_versions', 'component_hashes', 'state_hash'])
0103 stripped += 1
0104 print(f'\napplied: deleted {deleted}, stripped {stripped}')
0105 return 0
0106
0107
0108 if __name__ == '__main__':
0109 sys.exit(main())