trainlog/tools/export_training_feedback.py

35 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Export the bounded, direction-neutral TRAINING_FEEDBACK_V2 companion."""
import argparse, datetime, json, sqlite3
from trainlog_sqlite import connect_database
from pathlib import Path
def instant(value): return datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
def main(complete_causal_envelope=False):
parser=argparse.ArgumentParser(); parser.add_argument("output",type=Path); parser.add_argument("--database",type=Path,required=True); args=parser.parse_args()
db=connect_database(args.database)
try:
supported_versions = range(15, 36)
if db.execute("PRAGMA user_version").fetchone()[0] not in supported_versions:
raise ValueError("schema desktop v15-v35 requis")
if not complete_causal_envelope and db.execute("PRAGMA user_version").fetchone()[0] >= 20 and db.execute("SELECT 1 FROM sync_causal_state WHERE target_kind='feedback' AND deleted=1 LIMIT 1").fetchone(): raise ValueError("causal feedback protection requires the staged artifact")
roots=db.execute("SELECT f.feedback_id,s.session_id,se.entry_id,e.exercise_id,f.observed_at FROM exercise_feedback f JOIN session_exercises se ON se.id=f.session_exercise_row_id JOIN sessions s ON s.id=se.session_row_id JOIN exercises e ON e.id=se.exercise_row_id").fetchall()
followups=db.execute("SELECT f.followup_id,s.session_id,f.observed_at FROM session_followups f JOIN sessions s ON s.id=f.session_row_id").fetchall()
if len(roots)>4096 or len(followups)>4096: raise ValueError("trop d'observations")
exercise=[]
for row in roots:
revisions=db.execute("SELECT revision_id,created_at,raw_text FROM exercise_feedback_revisions WHERE feedback_id=?",(row[0],)).fetchall()
revisions.sort(key=lambda r:(instant(r[1]),r[0].encode()))
exercise.append(dict(zip(("feedback_id","session_id","entry_id","exercise_id","observed_at"),row)) | {"revisions":[dict(zip(("revision_id","created_at","raw_text"),r)) for r in revisions]})
result_followups=[]
for row in followups:
revisions=db.execute("SELECT revision_id,created_at,raw_text FROM session_followup_revisions WHERE followup_id=?",(row[0],)).fetchall()
revisions.sort(key=lambda r:(instant(r[1]),r[0].encode()))
result_followups.append(dict(zip(("followup_id","session_id","observed_at"),row)) | {"revisions":[dict(zip(("revision_id","created_at","raw_text"),r)) for r in revisions]})
exercise.sort(key=lambda v:(instant(v["observed_at"]),v["feedback_id"].encode())); result_followups.sort(key=lambda v:(instant(v["observed_at"]),v["followup_id"].encode()))
payload={"format":"trainlog-training-feedback","version":2,"generated_at":datetime.datetime.now().astimezone().isoformat(),"exercise_feedback":exercise,"session_followups":result_followups}
args.output.write_text(json.dumps(payload,ensure_ascii=False,separators=(",",":")),encoding="utf-8")
finally: db.close()
print(f"TRAINING_FEEDBACK_EXPORT=PASS version=2 exercise={len(exercise)} followups={len(result_followups)}")
if __name__=="__main__": main()