Extraction wrote straight into `questions`, so a machine's first attempt took a permanent id the moment it was produced. Ids come from a sequence and are never reissued: every rejected draft burned one, and every draft that needed fixing was sitting in the bank while it was being fixed. A run now lands in a batch of drafts with their own table and their own sequence. They are read, corrected and decided there, and `accept` is the only place a Question is created — a copy rather than a translation, because every field a draft holds is a field a question has, so nothing is lost at the moment of acceptance. Accepting is all or nothing, and everything is checked before anything is created: a call that reports failure must not leave questions behind from the drafts it got through first. My own test caught that — the first question existed before the second draft was refused. Readiness is reported for every draft rather than only on the attempt to accept it, so a reviewer sees what needs work before opening anything. A decided draft keeps its row and records what it became, so a batch reads as a history of what was decided rather than emptying as it is worked through. An acceptance cannot be undone from here: the question exists, and deciding twice would make a second one. No embeddings for drafts. A vector is for finding a question in the bank, and a draft is not in the bank. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
73 lines
3.7 KiB
Python
73 lines
3.7 KiB
Python
"""A staging area for extracted questions.
|
|
|
|
Extraction wrote straight into `questions`, so a machine's first attempt took a
|
|
permanent id the moment it was produced — and ids come from a sequence that
|
|
never reissues one, so every rejected draft burned an id and every draft that
|
|
needed fixing was in the bank while it was being fixed.
|
|
|
|
Drafts have their own table and their own sequence now. Nothing reaches the
|
|
bank, or takes a question id, until somebody accepts it.
|
|
|
|
Revision ID: c9d0e1f2a3b4
|
|
Revises: b8c9d0e1f2a3
|
|
"""
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision = "c9d0e1f2a3b4"
|
|
down_revision = "b8c9d0e1f2a3"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"draft_batches",
|
|
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
|
sa.Column("title", sa.String(200), nullable=False),
|
|
sa.Column("document_id", sa.Integer(), sa.ForeignKey("pdf_documents.id", ondelete="SET NULL"), nullable=True),
|
|
sa.Column("section_id", sa.Integer(), sa.ForeignKey("sections.id", ondelete="SET NULL"), nullable=True),
|
|
sa.Column("job_id", sa.String(64), nullable=True),
|
|
sa.Column("model_id", sa.String(160), nullable=True),
|
|
sa.Column("extraction_mode", sa.String(40), nullable=True),
|
|
sa.Column("category_id", sa.Integer(), sa.ForeignKey("question_categories.id", ondelete="SET NULL"), nullable=True),
|
|
sa.Column("created_by", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
|
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
sa.Column("status", sa.String(20), server_default="open"),
|
|
)
|
|
op.create_index("ix_draft_batches_job_id", "draft_batches", ["job_id"])
|
|
op.create_index("ix_draft_batches_status", "draft_batches", ["status"])
|
|
|
|
op.create_table(
|
|
"draft_questions",
|
|
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
|
sa.Column("batch_id", sa.Integer(), sa.ForeignKey("draft_batches.id", ondelete="CASCADE"), nullable=False),
|
|
sa.Column("position", sa.Integer(), server_default="0"),
|
|
sa.Column("question_text", sa.Text(), nullable=False),
|
|
sa.Column("question_type", sa.String(), nullable=False, server_default="mcq"),
|
|
sa.Column("options", sa.JSON(), nullable=True),
|
|
sa.Column("correct_answer", sa.String(), nullable=True),
|
|
sa.Column("explanation", sa.Text(), nullable=True),
|
|
sa.Column("option_explanations", sa.JSON(), nullable=True),
|
|
sa.Column("key_points", sa.JSON(), nullable=True),
|
|
sa.Column("attending_tip", sa.Text(), nullable=True),
|
|
sa.Column("difficulty", sa.String(10), nullable=True),
|
|
sa.Column("page_reference", sa.Integer(), nullable=True),
|
|
sa.Column("image_path", sa.String(), nullable=True),
|
|
sa.Column("explanation_image_path", sa.String(), nullable=True),
|
|
sa.Column("category_id", sa.Integer(), sa.ForeignKey("question_categories.id", ondelete="SET NULL"), nullable=True),
|
|
sa.Column("status", sa.String(20), server_default="pending"),
|
|
sa.Column("question_id", sa.Integer(), sa.ForeignKey("questions.id", ondelete="SET NULL"), nullable=True),
|
|
sa.Column("note", sa.Text(), nullable=True),
|
|
sa.Column("decided_by", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
|
sa.Column("decided_at", sa.DateTime(), nullable=True),
|
|
sa.Column("edited", sa.Integer(), server_default="0"),
|
|
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
)
|
|
op.create_index("ix_draft_questions_batch_id", "draft_questions", ["batch_id"])
|
|
op.create_index("ix_draft_questions_status", "draft_questions", ["status"])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table("draft_questions")
|
|
op.drop_table("draft_batches")
|