Compare commits

...
3 Commits
Author SHA1 Message Date
kcarandClaude Opus 4.8 e48dd73fdf exam: capture sections + choice_groups + marks_confidence in extraction_meta (WS-2 R3)
api-ci-deploy / test-build-deploy (push) Has been cancelled
Persist the paper's question-layout signals (A-level Section A/B, EITHER/OR choice
groups, marks confidence) onto exam_templates.extraction_meta so the setup UI can
show them. Data was carried by the contract but dropped at persistence.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GruxHXxfdp4kZCgAMVFgvV
2026-07-04 13:13:45 +00:00
kcarandClaude Opus 4.8 547836e04b exam: persist exam_response_areas.meta on canvas replace-save (WS-2 item 4)
api-ci-deploy / test-build-deploy (push) Has been cancelled
The full-replace save dropped the rich region meta (figure name/description, OMR
geometry). Add meta to ResponseAreaPayload + the replace insert so a named context
figure survives a round-trip. Pairs with the app carrying name/description through.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GruxHXxfdp4kZCgAMVFgvV
2026-07-04 12:13:13 +00:00
kcarandClaude Opus 4.8 df128508a3 exam: persist command_word + preamble from analyse contract v2 (WS-2)
_map_service_contract_to_rows now carries the two v2 fields onto exam_questions
ghost rows (command_word on leaf parts, preamble jsonb). Requires supabase
migration 78; applied to dev .94.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GruxHXxfdp4kZCgAMVFgvV
2026-07-04 11:56:40 +00:00
2 changed files with 11 additions and 0 deletions
+3
View File
@@ -80,6 +80,9 @@ class ResponseAreaPayload(BaseModel):
] = None ] = None
# Optional Context differentiation (v1 generic; future graph/chart/data_table/diagram/code_block/passage). # Optional Context differentiation (v1 generic; future graph/chart/data_table/diagram/code_block/passage).
context_type: Optional[str] = None context_type: Optional[str] = None
# Rich recognition payload (75-exam-marker-region-meta.sql): figure name/description, OMR geometry, unit…
# Carried on canvas save so a named context figure survives a round-trip.
meta: Optional[Dict[str, Any]] = None
source: Literal["manual", "ai"] = "manual" source: Literal["manual", "ai"] = "manual"
confirmed: bool = True confirmed: bool = True
confidence: Optional[float] = Field(default=None, ge=0, le=1) confidence: Optional[float] = Field(default=None, ge=0, le=1)
+8
View File
@@ -745,6 +745,9 @@ def _map_service_contract_to_rows(template_id: str, contract: Dict[str, Any],
"bounds": _frac_box_to_canvas(q.get("bounds"), q.get("page") or 1, pages), "bounds": _frac_box_to_canvas(q.get("bounds"), q.get("page") or 1, pages),
"page": q.get("page"), "source": "ai", "confirmed": False, "page": q.get("page"), "source": "ai", "confirmed": False,
"confidence": _safe_confidence(q.get("confidence")), "derivation": "extract-service", "confidence": _safe_confidence(q.get("confidence")), "derivation": "extract-service",
# analyse contract v2 (migration 78): command verb + stem prose per part
"command_word": (q.get("command_word") or None) if not q.get("is_container") else None,
"preamble": q.get("preamble") or None,
}) })
for q in questions: # FK safety: de-parent a dangling parent_id for q in questions: # FK safety: de-parent a dangling parent_id
if q["parent_id"] and q["parent_id"] not in q_ids: if q["parent_id"] and q["parent_id"] not in q_ids:
@@ -799,6 +802,10 @@ def _run_service_extract_merge(ctx: ExamContext, template_id: str, pdf_bytes: by
updates: Dict[str, Any] = {"extraction_meta": { updates: Dict[str, Any] = {"extraction_meta": {
"engine": "extract-service", "slug": slug, "audit": meta.get("audit") or {}, "engine": "extract-service", "slug": slug, "audit": meta.get("audit") or {},
"counts": {k: len(v) for k, v in rows.items()}, "counts": {k: len(v) for k, v in rows.items()},
# question-layout signals for the setup UI: paper sections (e.g. A-level Section A/B) + EITHER/OR choices
"sections": meta.get("sections") or [],
"choice_groups": meta.get("choice_groups") or [],
"marks_confidence": meta.get("marks_confidence"),
}} }}
n_pages = meta.get("n_pages") or meta.get("pages") n_pages = meta.get("n_pages") or meta.get("pages")
if n_pages: if n_pages:
@@ -1140,6 +1147,7 @@ async def replace_template(
"kind": ra.kind, "kind": ra.kind,
"response_form": ra.response_form, "response_form": ra.response_form,
"context_type": ra.context_type, # 73: optional Context differentiation "context_type": ra.context_type, # 73: optional Context differentiation
"meta": ra.meta, # 75: rich recognition payload (name/description/OMR/…)
"source": ra.source, "source": ra.source,
"confirmed": ra.confirmed, "confirmed": ra.confirmed,
"confidence": ra.confidence, "confidence": ra.confidence,