74 lines
4.2 KiB
Python
74 lines
4.2 KiB
Python
"""Targeted recovery. Stop backend first. Default is read-only inspection."""
|
|
import argparse
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
EXPECTED = {'Manajemen Informatika', 'Manajemen Informatika Internasional',
|
|
'Teknik Komputer Internasional', 'Teknik Informatika Internasional'}
|
|
REASON = 'Input keliru: empat nama prodi digunakan sebagai ulasan. Tidak boleh diaktifkan.'
|
|
|
|
def repair(store, apply=False):
|
|
path = store / 'registry.sqlite3'
|
|
if not path.is_file():
|
|
raise ValueError('Registry tidak ditemukan.')
|
|
db = sqlite3.connect(path.as_uri() + ('?mode=rw' if apply else '?mode=ro'), uri=True)
|
|
db.row_factory = sqlite3.Row
|
|
try:
|
|
if apply:
|
|
db.execute('BEGIN IMMEDIATE')
|
|
versions = {r['id']: json.loads(r['metadata']) for r in db.execute('SELECT * FROM versions')}
|
|
if set(versions) != {'v1', 'v2', 'v3'}:
|
|
raise ValueError('Versi berbeda dari kondisi yang diperiksa. Pemulihan dibatalkan.')
|
|
if db.execute("SELECT COUNT(*) FROM jobs WHERE status IN ('queued','running')").fetchone()[0]:
|
|
raise ValueError('Masih ada training berjalan. Hentikan dan periksa dahulu.')
|
|
rows = [dict(r) for r in db.execute("SELECT * FROM additions WHERE version='v3'")]
|
|
count = db.execute('SELECT COUNT(*) FROM additions').fetchone()[0]
|
|
if not rows and versions['v3'].get('invalidated') and count == 49:
|
|
print('Sudah dipulihkan. Kumpulan data latih 505; v3 tidak valid.')
|
|
return
|
|
if len(rows) != 4 or {r['text'] for r in rows} != EXPECTED or count != 53:
|
|
raise ValueError('Data tidak cocok dengan empat prodi yang diperiksa. Tidak ada perubahan.')
|
|
if versions['v2']['jumlah_data_training'] != 505:
|
|
raise ValueError('Metadata v2 tidak sesuai.')
|
|
for name in ('model_logistic_regression.joblib', 'tfidf_vectorizer.joblib'):
|
|
if not (store / 'v2' / name).is_file():
|
|
raise ValueError('Berkas model v2 tidak lengkap.')
|
|
print('Terverifikasi: 4 nama prodi pada v3; 509 -> 505 data latih.')
|
|
if not apply:
|
|
print('Pemeriksaan saja. Belum ada data yang diubah. Jalankan dengan --apply untuk memulihkan.')
|
|
return
|
|
# Make an SQLite-consistent recovery copy before changing any rows.
|
|
backup = store.parent / ('registry_sebelum_pemulihan_' + datetime.now().strftime('%Y%m%d_%H%M%S_%f') + '.sqlite3')
|
|
source = sqlite3.connect(path.as_uri() + '?mode=ro', uri=True)
|
|
dest = sqlite3.connect(backup)
|
|
try:
|
|
source.backup(dest)
|
|
finally:
|
|
source.close(); dest.close()
|
|
db.execute('CREATE TABLE IF NOT EXISTS quarantined_additions (text_key TEXT PRIMARY KEY, record_json TEXT NOT NULL, reason TEXT NOT NULL, quarantined_at TEXT NOT NULL)')
|
|
stamp = datetime.now(timezone.utc).isoformat()
|
|
for row in rows:
|
|
db.execute('INSERT INTO quarantined_additions VALUES (?,?,?,?)', (row['text_key'], json.dumps(row), REASON, stamp))
|
|
db.execute('DELETE FROM additions WHERE text_key=? AND version=?', (row['text_key'], 'v3'))
|
|
versions['v3'].update(invalidated=True, invalid_reason=REASON, invalidated_at=stamp)
|
|
db.execute("UPDATE versions SET metadata=? WHERE id='v3'", (json.dumps(versions['v3']),))
|
|
db.execute("UPDATE settings SET value='v2' WHERE key='active'")
|
|
db.execute("UPDATE jobs SET message=? WHERE version='v3'", ('Model v3 tidak valid: input prodi. Empat data telah dikarantina; kumpulan training dipulihkan.',))
|
|
db.commit()
|
|
print('Selesai: 4 data dikarantina, kumpulan data latih 505, v2 aktif, v3 ditandai tidak valid.')
|
|
print('Berkas model dan riwayat analisis tidak dihapus. Cadangan tambahan:', backup)
|
|
finally:
|
|
db.close()
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--store', type=Path, default=Path(__file__).resolve().parent / 'model_store')
|
|
parser.add_argument('--apply', action='store_true')
|
|
args = parser.parse_args()
|
|
try:
|
|
repair(args.store.resolve(), args.apply)
|
|
except (ValueError, sqlite3.Error) as error:
|
|
raise SystemExit(str(error))
|