Appearance
Import Service API (docx-import-service alias)
Baseline Endpoints
POST /v1/import/docx/qasPOST /v1/import/docx/qas/streamPOST /v1/import/docx/qas/jobsPOST /v1/import/mathtype/convertPOST /v1/import/mathtype/jobsGET /v1/import/mathtype/jobs/{id}/downloadPOST /v1/import/mathtype/jobs/{id}/reprocessPOST /v1/import/docx/docx-fast-jobsPOST /v1/import/ocr/jobsPOST /v1/import/ocr/jobs/{id}/reprocessGET /v1/import/docx/docx-fast-jobs/{id}/temp-draftPOST /v1/import/docx/docx-fast-jobs/{id}/reprocessGET /v1/import/docx/teacher-libraryGET /v1/import/docx/jobsGET /v1/import/docx/jobs/{id}GET /v1/import/docx/jobs/{id}/statusGET /v1/import/docx/jobs/{id}/eventsGET /v1/import/docx/jobs/eventsGET /v1/admin/import/docx/queue-summaryPATCH /v1/import/docx/jobs/{id}/reviewPOST /v1/import/docx/jobs/{id}/approveGET /healthzGET /readyz
SVC-012 Import Route Proof Bundle
The bounded SVC-012-import-route-proof-bundle contract is recorded in Import Route Proof Bundle. Native status, approval, detail, and review-save routes are covered by non-default route tables and review roundtrip tests. Default /api/exam-import* traffic remains legacy-proxied until real live/browser and rollback evidence is captured.
Current Behavior
The service validates multipart field file, requires .docx, checks the ZIP-based Office magic header, enforces a 50MB upload limit, and returns latency_ms.
Parser mode:
- If
GO_FORMULA_DOCX_URLorGO_FORMULA_DOCX_BASE_URLis set, the service streams the upload once to{baseUrl}/v1/import/docx/qas. The adapter reads the QAS envelope rather than the compactsimpleresponse so it retains each formula's deterministic conversion provenance.GO_FORMULA_DOCX_TIMEOUT_MSdefaults to180000; an injected HTTP client with no timeout is cloned with that deadline while preserving its transport, so import requests cannot wait indefinitely. - Local Compose, raw offline K8s, and Helm now set
GO_FORMULA_DOCX_URLtohttp://go-formula-docx:8080by default and run the existing parser binary as an internalgo-formula-docxruntime. The legacy parser source stays read-only. - If no Go Formula URL is configured, the baseline parser returns a warning-only response with
PARSER_ENGINE_NOT_CONNECTEDinstead of silently dropping content.
Media materialization:
- If
DOCUMENT_SERVICE_URLorDOCUMENT_SERVICE_BASE_URLis set, the service extracts referenced DOCX package images and rewrites materialized references to/api/storage/media-assets/{id}/content. It posts image bytes to document-service/v1/internal/media-assetswithX-Document-Artifact-Token; production requiresDOCUMENT_ARTIFACT_SERVICE_TOKENand has no shared-token or legacy-presigned fallback. - The materializer requires
X-Organization-Idfor upload ownership and passesX-User-Idas the owner when available. - Production rejects DOCX Fast storageKey-only reads. Native jobs reopen their owner-scoped source asset, while media packaging reads
/v1/internal/media-assets/{id}/contentwith the artifact token. - If a referenced image cannot be extracted, uploaded, or registered, the original media reference is preserved and the response includes
GO_FORMULA_DOCX_IMAGE_MATERIALIZE_FAILED; the service must not silently drop the image. stats.total_imagesuses Go FormularenderableImageCountwhen available, otherwise derives only renderable image candidates from QAS question media. WMF/OLE formula artifacts are not counted as normal browser images; their raw references stay on the formula provenance record.
Required Parser Follow-Up
The Go Formula adapter now maps direct service output into the QAS response, including questions[].equations source metadata, can materialize extractable package images into document-service, applies payload-level DOCX style answer hints, can build annotated fallback questions when upstream returns zero questions, and has native OOXML passes for template-gated DOCX files. Physics 28Q, Math 22Q, English 40Q, and DGNL 102Q corpus QAS shapes are covered by an opt-in runtime integration test.
Formula provenance and review:
- Each
formulas[]record retains LaTeX, MathML, source kind, package path, OLE/preview asset IDs, MTEF checksum, parser schema/SDK/algorithm versions, confidence, deterministic flag, and runtime metadata where present. - Formula counts use the per-question
equations[]collection, not the QAS object-levelstats.equationCount, because the latter also counts source objects repeated in answer/solution rendering. This preserves the corpus metrics of Physics84and Math125while retaining every question-attached formula reference. - Deterministic OMML/MTEF formulas remain approval-safe metadata. Missing LaTeX, non-LaTeX format, non-deterministic conversion, or runtime formula warnings create
GO_FORMULA_DOCX_FORMULA_REVIEW_REQUIRED; they remain visible for review and must not be represented as successful Word Equation conversion. formulaRefsin the question-bank/exam approval payload carry this provenance insourceJson, including review flags. Question Bank persists it inquestion_formula_refs.source_jsonandquestion_versions.formula_refs_json; Exam keeps the immutable formula fields and source metadata in its own snapshot. OLE/WMF preview assets are not materialized as normal browser images merely because they belong to a valid formula.POST /v1/import/mathtype/convertis an internal, direct DOCX conversion boundary. It calls the same QAS runtime once, replaces only deterministicmtef-*OLE relationships with OMML, and returns an ephemeral*-word-equation.docxattachment. It reportsX-Hoctapaz-MathType-Converted,Skipped,Unresolved, andWarningsresponse headers; missing LaTeX, non-deterministic formulas, and unmatched relationships remain untouched in the output DOCX.- The native OMML writer supports common text, Greek/operator commands, scripts, fractions, and square roots. Unsupported LaTeX uses
MATHTYPE_WORD_EQUATION_LINEAR_FALLBACKand requires teacher review. A structurally valid DOCX is not a claim of Microsoft Word visual-equivalence for every expression. POST /v1/import/mathtype/convertintentionally remains an ephemeral direct response for diagnostics. The durable path isPOST /v1/import/mathtype/jobs: it reopens an organization-scoped DOCX fromdocument-service, spools it under the import job, invokes the converter once, and posts the output only to document-service's token-gated/v1/internal/generated-artifactsboundary. Import-service stores generated media-asset metadata, conversion counters, parser versions, and warning count; it never writes object storage directly.- Completed jobs are filtered as
source=mathtype-equation, expose artifact metadata through the standard job detail, and stream bytes throughGET /v1/import/mathtype/jobs/{id}/download. Processing jobs found after restart fail withMATHTYPE_RECOVERY_REQUIRES_RETRY;POST /v1/import/mathtype/jobs/{id}/reprocessdeliberately reads the original source and clears previous artifact metadata before retrying.DOCUMENT_ARTIFACT_SERVICE_TOKENmust match document-service for this workflow. POST /v1/import/docx/jobs/{jobId}/approverejects an import when a question that needs an answer has no parser/style-hint/review answer, or when a single-choice question carries more than one answer label. The teacher must save a corrected review result before the canonical question-bank or exam handoff; import-service does not invent answer keys or silently reclassify a conflicting single-choice payload as multiple-choice. For a newEXAM_DRAFT, it also rejects unsupportedworkflowordeliveryModevalues before calling Question Bank, so those invalid enum values cannot create canonical rows.- OCR completion stores editor-facing
sourceTextplussections[]and remains review-required. After a teacher saves that review, approval derives canonicalquestions[]for the Question Bank owner API, including stem/options, reviewed answer labels, solution HTML, source-text spans, andsourceKind=ocr-document. It does not change the editor JSON shape or allow the original unreviewed OCR output to call Question Bank. EXAM_DRAFTapproval uses that same reviewedparseResultfor both the Question Bank ingest and Exam snapshot payload. A review may correct, reorder, or retain only a subset of parsed questions; snapshot IDs, content, answer keys, explanations, media, formulas, andsourceSnapshotJsonmust therefore come from the reviewed rows, not the original parser result. A mismatched Question Bank ID count is rejected before writing snapshots. If snapshot persistence fails, Import retains the owner writes and resumes only from its durable approval checkpoint; no service database is accessed directly and no owner data is deleted as compensation.- The OCR/MathType/Teacher DOCX Fast workflows have non-default Gateway candidate tables. The browser uploads only to
/api/teacher/import-sources, then passes the opaquesourceAssetIdto the native create routes. OCR rawstorageKeyinput is rejected; OCR detail, status, event log, review, reprocess, and approval are actor-scoped because the result can contain recognized source text. Default/api/exam-import*traffic remains legacy-proxied. The isolated direct OCR browser proof is recorded indocs/qa/ocr-review-browser-local-postgres.md; it is not default-route, production, batch-mode, or provider-quality promotion evidence.
Classifier baseline:
internal/classifierprovides a deterministic question-type fallback for obvious shapes: explicit type hints, true/false sub-items, multiple correct answers, single-choice options, short numeric answers, essay prompts, and media/table/graph review cases.- This is not a replacement for
ai-classifier-service; low-confidence or media/table/graph classifications return warnings so review can keep the source evidence visible.
Fallback and answer-hint parity:
- If the Go Formula payload includes
styleAnswerHintsordocxStyleAnswerHints, the adapter applies them to QAScorrect_answerand recordsanswer_evidence=DOCX_STYLE_HINTinsource_location. - If Go Formula returns zero questions but includes
annotatedResult.segmentsorannotatedSegments, the adapter builds warning-safe QAS questions from those annotated segments and emitsGO_FORMULA_DOCX_ANNOTATED_FALLBACK. - The native OOXML style-hint pass reads
word/document.xmlfrom the uploaded DOCX, detects red/underlined answer runs for Math/English templates and yellow-highlighted answer runs for DGNL templates, then applies hints withDOCX_NATIVE_STYLE_ANSWER_HINT_APPLIED. - If the DGNL 102Q runtime payload returns zero QAS questions, the native fallback builds
DGNL_SINGLE_CHOICEquestions from DOCX A-D option groups, answer-only->rows, and review-required unlabeled choice rows, then emitsDOCX_NATIVE_DGNL_FALLBACK. make test-docx-corpusruns the opt-in Physics/Math/English/DGNL QAS parity checks whenGO_FORMULA_DOCX_URLandHOCTAPAZ_DOCX_CORPUS_DIRpoint to the local runtime/corpus.make test-docx-materializationruns an opt-in Physics 28Q runtime check through livedocx-import-serviceplusdocument-service, asserts7materializedMediaAssetreferences, and fetches each asset back through/v1/storage/media-assets/{id}/content.DOCX_MATERIALIZATION_SELF_TEST=1 make test-docx-materializationruns the same Physics DOCX through the real Go Formula parser and the import-service materializer with an in-memory uploader. It verifies seven scoped image uploads and stable media references without requiring a live import or document service.make test-docx-warning-captureplusmake test-docx-warning-paritycompares captured native Physics/Math/English/DGNL response warnings against legacyoutput/import-audit-20260702/apiartifacts. The filtered comparator passes withDOCX_WARNING_PARITY_ALLOW_EXTRA_NATIVE=1, because native emits additional review-safety warnings rather than claiming exact no-extra warning counts.
Job Model
The job endpoints use a repository interface:
- If
DATABASE_URLis configured and reachable, the service uses the pgx-backeddocx_import_jobsanddocx_import_eventstables fromservices/docx-import-service/migrations/000002_import_jobs.sql, pluspayload_reffromservices/docx-import-service/migrations/000003_import_payload_spool.sql. - If no database is configured, the service uses an in-memory repository so local skeleton runs remain possible.
Job status values:
PENDINGPROCESSINGCOMPLETEDFAILED
GET /v1/admin/import/docx/queue-summary is the owner projection for admin operations queue.failures and queue.depths. It returns sourceService=docx-import-service, job counts by status, a queues array with the native queue sample and the optional BullMQ bridge sample when configured, recent failed jobs capped by recentFailuresLimit (1..50, default 10), and retryCommandApi=/v1/import/docx/docx-fast-jobs/{id}/reprocess. Admin-service and BFF probes consume this projection; they must not read docx_import_jobs, payload spool files, Redis BullMQ keys, or import-service memory state directly.
Events currently emitted:
job.queuedjob.startedjob.completedjob.failedapproval.completedauto_approval.completedauto_approval.failed
POST /v1/import/docx/qas/jobs now writes the uploaded DOCX to the service-local payload spool, stores only an internal payload_ref on the PENDING job, and returns quickly. A bounded in-process worker queue then moves the job to PROCESSING, opens the payload reference, parses it in the background, and deletes the spool file after COMPLETED or FAILED. DOCX_IMPORT_PAYLOAD_DIR sets the spool directory; if unset, the service uses a temp-directory fallback. DOCX_IMPORT_WORKER_CONCURRENCY controls the non-OCR native worker count; if unset, the service falls back to ALGORITHM_IMPORT_WORKER_CONCURRENCY and then default 2. Direct OCR uses the separate docx-import-ocr-direct lane. DOCX_IMPORT_OCR_WORKER_CONCURRENCY controls that lane, defaults to 1, and is capped at 10, so a slow direct provider request cannot hold a DOCX worker. Opt-in batch polling uses a separate docx-import-ocr-batch lane controlled by DOCX_IMPORT_OCR_BATCH_WORKER_CONCURRENCY (default 1, maximum 10) and DOCX_IMPORT_OCR_BATCH_POLL_INTERVAL_MS (default 5000, bounded to 1000..60000).
On startup, the service scans PENDING and PROCESSING jobs with payload refs and re-enqueues them. If the payload reference is missing, it marks the job FAILED with DOCX_IMPORT_PAYLOAD_MISSING instead of leaving it stuck.
POST /v1/import/mathtype/jobs is the durable Word Equation conversion path. The Gateway candidate requires an opaque owner-scoped sourceAssetId; raw storageKey is rejected for this MathType job path. It uses a namespaced idempotency key and records parser schema/version plus scanned, converted, skipped, and unresolved counts. On completion it holds only generated artifact metadata; the DOCX bytes and authorization remain document-service-owned. It never auto-approves questions or exams. GET /v1/import/mathtype/jobs/{id}/download streams the generated asset through document-service only for the job actor; the same actor is required for explicit MathType reprocess.
POST /v1/import/docx/docx-fast-jobs is the legacy-compatible JSON create adapter for POST /api/exam-import/docx-fast-jobs. It accepts either the owner-scoped sourceAssetId or legacy questionStorageKey, plus optional questionFileName/fileName, title, autoApproveToQuestionBank, subjectId, and gradeLevel metadata. The sourceAssetId candidate reopens the DOCX through document-service's internal source boundary with exact organization, actor, purpose, and docx-fast kind. Raw storage keys remain a legacy compatibility input. When Idempotency-Key is present, the service trims it, caps it at 120 characters, and returns the recent matching native job within 15 minutes for the same organization, actor, and source reference before fetching storage or enqueueing again. It validates the DOCX extension and ZIP magic before reusing the native queue/payload spool path. When autoApproveToQuestionBank is true, a successfully parsed native DOCX Fast job triggers asynchronous QUESTION_BANK approval and records either auto_approval.completed or auto_approval.failed without changing the normal job completion status. The response is a legacy success envelope with a camelCase job summary so the frontend api<T>() helper can continue to unwrap data.
Question Bank approval accounting is disabled by default. Set DOCX_IMPORT_APPROVAL_USAGE_ENABLED=true only with USAGE_SERVICE_BASE_URL (or USAGE_SERVICE_URL) configured. Every native QUESTION_BANK approval then checks and consumes the completed reviewed questions count using the durable approval request digest; it never debits the AZ Credit wallet. DOCX_IMPORT_APPROVAL_USAGE_TIMEOUT_MS defaults to 3000. The older DOCX_FAST_AUTO_APPROVAL_USAGE_* names remain compatibility aliases. Compose, raw K8s, and Helm ship the canonical flag as false so a rollout must explicitly opt in before any existing approval is re-metered.
Internal Mistral OCR
POST /v1/import/ocr/jobs is a PDF/image OCR job endpoint. The Gateway candidate requires an opaque owner-scoped sourceAssetId; raw storageKey is rejected. It accepts only .pdf, .png, .jpg, .jpeg, and .webp sources up to 50MB. The service passes the source to Mistral's POST /v1/ocr as a base64 data URL, using MISTRAL_API_KEY, optional MISTRAL_OCR_BASE_URL, and optional MISTRAL_OCR_MODEL (mistral-ocr-latest by default). Omitted ocrMode and ocrMode=direct retain that direct call. ocrMode=mistral-batch is an explicit opt-in: import-service uploads the OCR source and a JSONL input file, creates a Mistral /v1/ocr batch, then polls and reads its output file. Its service-owned ocr_batch_json state persists provider source/input/output IDs, batch ID, lease, start/poll attempts, and next poll time. The same idempotency key is separated from direct mode.
The completed job stores Mistral Markdown, per-page metadata, model, provider, and usage fields in service-owned review JSON. It is explicitly REVIEW_REQUIRED; it never auto-approves to question-bank or creates an exam. Teachers can correct the generated review payload through the existing review route before a question-bank approval. OCR direct requests that were PROCESSING during a service restart become OCR_RECOVERY_REQUIRES_RETRY instead of being sent to Mistral again. A batch job restarts only its poll when a provider batch ID is already persisted. A batch Start interrupted before that durable ID is failed with OCR_BATCH_START_RECOVERY_REQUIRES_RETRY; use POST /v1/import/ocr/jobs/{id}/reprocess to deliberately retry from the stored document-service source. Reprocess requires the actor that created the job and preserves the persisted direct or batch mode, so an opt-in batch job starts/polls a new batch rather than falling through to direct OCR.
The direct OCR lane is deliberately separate from DOCX/MathType work and from the opt-in batch lane. This isolates a slow direct Mistral request and keeps batch poll traffic from consuming direct OCR workers. Direct calls still never auto-replay after restart.
OCR accounting is disabled unless DOCX_IMPORT_OCR_USAGE_WALLET_ENABLED=true. When enabled, the worker calls usage-service and wallet-service only through their internal owner APIs. Before the provider call it checks one ocr_pages_monthly unit and the AZ Credit balance for DOCX_IMPORT_OCR_CREDITS_PER_PAGE (default 1). A usable result then checks the exact len(pages), creates an idempotent wallet USAGE_DEBIT, and consumes the matching usage quota. The review payload records the settled accounting metadata but import-service does not own usage counters or wallet rows.
The keys are stable for the job ID: ocr-import:wallet:{jobId} and ocr-import:usage:ocr_pages_monthly:{jobId}. A failed post-provider settlement leaves the OCR job failed without a review payload; an explicit reprocess may call the provider again, but reuses those owner-service keys so a completed debit or consume cannot be duplicated. Enabled configuration requires USAGE_SERVICE_BASE_URL, WALLET_SERVICE_BASE_URL, and valid positive cost / timeout values; invalid configuration exits the service before it serves jobs.
This does not claim legacy batch OCR, real public-provider behavior, or default gateway/browser promotion. The non-default candidate grants no evidence that the broad import routes may leave legacy-proxy state.
Batch Parity Boundary
The legacy Mistral workflow and the official Batch API both use provider file upload, JSONL input_files, /v1/ocr batch submission, job polling, and an output file. Native import-service implements this only for the explicit ocrMode=mistral-batch contract. It persists provider file and batch IDs plus lease, attempt, and next-run state. Because the public create-job contract does not state an idempotency guarantee, restart recovery resumes Poll only after the provider batch ID is durable and fails closed during an unfinished Start instead of submitting a second batch.
make test-import-pdf-ocr-workflow is the repeatable internal workflow check. It uses a Mistral-compatible local provider to verify the direct data-URL and batch file/JSONL submit/poll/output requests, persisted review metadata, document-service source re-open, and explicit reprocess without using a billable provider credential. The separate browser candidate proof is make test-ocr-batch-review-browser-local-postgres; it remains local and non-default.
The same target also covers the opt-in OCR usage/wallet client and worker ordering. See OCR Usage Wallet Contract Proof for its precise verification and rollout boundary.
GET /v1/import/docx/docx-fast-jobs/{id}/temp-draft is the legacy-compatible materialized temp-draft read branch. For completed native DOCX Fast jobs with a stored parse result, it returns token, jobId, expiresAt, parseResult, and an empty assets array with message DOCX Fast materialized draft. If a reviewed parse result has been saved through PATCH /v1/import/docx/jobs/{id}/review, the temp-draft read returns the reviewed payload so manual question-type overrides survive reload.
PATCH /v1/import/docx/jobs/{id}/review is the native backend foundation for legacy review save. It accepts parseResult plus optional title and sourceText, stores the reviewed parse result JSON verbatim in the service-owned job row, and returns a legacy success envelope with parseResultJson. When sourceText is provided together with an object-shaped reviewed parseResult, compatibility responses overlay it as parseResultJson.sourceText so the source editor can reload the same text after save while the raw reviewed JSON remains unchanged in storage. This preserves frontend metadata such as sourceMetadataJson.questionTypeManualOverride, sourceMetadataJson.questionTypeReviewed, and sourceMetadataJson.questionTypeAutoDetected=false without coercing the payload through the QAS struct. The endpoint records review.updated events. When sourceText is provided without parseResult, the native endpoint runs a baseline source-text reparse for simple Câu n questions with A/B/C/D options, * answer markers, Đáp án:, and Lời giải:. The generated parseResultJson includes a warning that complex legacy parser parity is still pending. When docxFastTempDraftToken is provided, the native endpoint accepts only the materialized token shape returned by native temp-draft reads: materialized-{jobId}. That token is treated as an idempotent acknowledgement that the native DOCX Fast parse payload is already materialized in the import job; arbitrary legacy temp-draft tokens and client-side temp assets remain on legacy routes until separate parity work.
GET /v1/import/docx/jobs/{id} returns the legacy-compatible detail reload shape with camelCase fields such as questionFileName, fileName, parseStatus, matchStatus, reviewStatus, packagingJson, and parseResultJson. If a reviewed result has been saved, parseResultJson returns the reviewed payload instead of the original parser output. Completed native DOCX Fast jobs include packagingJson.docxFastTempDraft with status MATERIALIZED, token materialized-{id}, and assets: [] temp-draft reads; this lets the editor skip legacy client temp-draft materialization for native materialized jobs.
GET /v1/import/docx/docx-fast-jobs/{id}/temp-draft/assets/{tempAssetId}/content is the materialized asset content adapter. It does not create a native non-materialized temp-draft store. Instead, it accepts only completed native DOCX Fast jobs, verifies that tempAssetId matches a materialized media reference in the stored parse result (media_asset_id, media id, go_asset_id, or the /api/storage/media-assets/{id}/content URL), then streams the permanent media asset through document-service /v1/internal/media-assets/{id}/content with X-Document-Artifact-Token and the legacy private cache header.
POST /v1/import/docx/docx-fast-jobs/{id}/reprocess is the legacy-compatible DOCX Fast reprocess adapter. It re-reads the source DOCX from the job's stored owner-scoped sourceMediaAssetID when present, otherwise its legacy questionStorageKey, through document-service, saves a fresh payload under the same native job id, clears previous parse output, requeues the native worker, and returns message DOCX Fast import reprocessed. The foundation rejects jobs that are already active or waiting in the native queue.
ImportJobService publishes these lifecycle events to a service-owned event bus. Without REDIS_URL, the bus is in-process only. When REDIS_URL is configured and reachable, the service wraps the in-process bus with Redis pub/sub on DOCX_IMPORT_EVENTS_CHANNEL (default docx-import:jobs) so subscribers on one service process can receive job.updated events from another process. Redis payloads redact the heavy parsed result body because the SSE summary does not need it. Uploaded DOCX bytes and payload refs are not published to Redis, SSE, or logs.
Legacy-Compatible Status And Events
Phase P3-005 maps legacy import status/history surfaces without cutting over the default public route table:
- Legacy
/api/exam-import/teacher-librarymaps to nativeGET /v1/import/docx/teacher-library. - Legacy
POST /api/exam-import/docx-fast-jobsmaps to nativePOST /v1/import/docx/docx-fast-jobsin the non-default gateway create-job example. - Legacy
GET /api/exam-import/docx-fast-jobs/{id}/temp-draftmaps to nativeGET /v1/import/docx/docx-fast-jobs/{id}/temp-draftonly for the materialized read branch in the same non-default gateway example. make test-import-temp-draft-liveis the read-only live smoke for the non-default DOCX Fast materialized temp-draft route. It requires a completed native DOCX Fast job with stored parse output, auth/org context, and a gateway running with the native import-create route table.- Legacy
/api/exam-import/docx-fast-jobs/{id}/temp-draft/assets/{tempAssetId}/contentmaps to nativeGET /v1/import/docx/docx-fast-jobs/{id}/temp-draft/assets/{tempAssetId}/contentonly for materialized media already referenced by the completed native job. make test-import-temp-asset-liveis the read-only live smoke for the non-default DOCX Fast materialized temp asset content route. It requires a completed native DOCX Fast job, a materialized temp asset id present in the stored parse result, auth/org context, and a gateway running with the native import-create route table.- Legacy
POST /api/exam-import/docx-fast-jobs/{id}/reprocessmaps to nativePOST /v1/import/docx/docx-fast-jobs/{id}/reprocessin the same non-default gateway example. make test-import-reprocess-liveis the opt-in write smoke for the non-default DOCX Fast reprocess route. It requires explicit confirmation, a native DOCX Fast job with a stored source DOCX, auth/org context, and a gateway running with the native import-create route table.make test-import-create-liveis the opt-in write smoke for the non-default DOCX Fast create route. It requires explicit confirmation, a real uploaded DOCXquestionStorageKey, auth/org context, and a gateway running with the native import-create route table.make test-import-create-browseris the opt-in Playwright smoke for the real import surface. It can either upload a caller-supplied DOCX through the UI file input or use an existingquestionStorageKey, then verifies the browser-observed gateway route headers and legacy success envelope.- Legacy
PATCH /api/exam-import/jobs/{id}/reviewmaps to nativePATCH /v1/import/docx/jobs/{id}/reviewonly in the non-default review-save gateway example; all sibling job actions remain legacy. - Legacy
GET /api/exam-import/jobs/{id}maps to nativeGET /v1/import/docx/jobs/{id}only in the non-default detail reload gateway example; deeper sibling paths such as/statusand/reviewremain legacy unless separately routed. - Legacy
/api/exam-import/jobs/{id}/statusand/api/imports/{id}/statusmap to nativeGET /v1/import/docx/jobs/{id}/status. - Legacy
/api/exam-import/algorithm-jobs/eventsmaps to nativeGET /v1/import/docx/jobs/events.
The compatibility list and SSE snapshot use the existing frontend summary fields: id, jobKind, source, title, questionFileName, fileName, status, fileStatus, extractionStatus, parseStatus, matchStatus, reviewStatus, progress, optional queue, errorSummary, and timestamps. Both paths require X-Organization-Id for native rows; missing organization context returns an empty rows/meta snapshot rather than a cross-organization fallback. Filtering and page/limit totals are evaluated in the job repository, not by capped in-memory post-filtering. The queue object is attached only while native jobs are PENDING or PROCESSING, with legacy-shaped fields such as status, waiting, active, totalPending, waitingPosition, jobsAhead, and workerConcurrency. The status endpoint returns the legacy status counters: status, totalQuestions, parsedQuestions, pendingEquations, pendingImages, warnings, and errors.
This is a read/status compatibility layer over native docx_import_jobs and docx_import_events. The SSE route now replays stored jobs, keeps the stream open, sends periodic heartbeat events, and forwards live job.updated events for job start/completion/failure/approval changes from the current process or Redis fanout. Browser cutover remains deferred until the gateway auth/header adapter, worker topology, and browser verification are ready. The payload spool is already wired to a named Compose volume and a docx-import-payloads PersistentVolumeClaim in offline K8s/Helm. A read-only BullMQ snapshot bridge can be enabled with DOCX_IMPORT_BULLMQ_BRIDGE_ENABLED=1 to report legacy exam-import-algorithm queue depth/position when a native job is not present in the in-process queue; it does not enqueue, remove, retry, or claim legacy jobs. Full shared worker execution with the legacy BullMQ queue remains deferred. make test-docx-bullmq-status is an opt-in live smoke for this bridge path; it checks the non-default status route table and the teacher-library row queue shape for a caller-supplied pending/processing job id. make test-docx-import-library-browser adds browser-route evidence for the real /teacher/exams/library page and can optionally search for the same job id to assert the queue snapshot in the browser-observed response.
Native Approval Boundary
POST /v1/import/docx/jobs/{id}/approve approves a completed job result into native services. It is an internal /v1 contract. A non-default API Gateway route-table example can adapt legacy-compatible POST /api/exam-import/jobs/{id}/approve to this endpoint, but the default route table keeps public /api/exam-import/* on legacy until gateway auth/RBAC and browser review parity are verified.
Question-bank approval:
json
{
"target": "QUESTION_BANK",
"questionMetadata": {
"subjectId": "math",
"gradeLevel": 12,
"tags": ["approved"]
}
}The service calls question-bank-servicePOST /v1/questions/import-docx-output, forwarding X-Organization-Id and X-User-Id, and adds sourceImportJobId, sourceFileName, and approval metadata. It also derives a stable Idempotency-Key from the approval operation and sends it to that Question Bank owner call. Question Bank persists the owner outcome with the source job and canonical request digest in the same transaction as the question graph. A retry therefore returns the original documentId and questionIds; reusing the key with a different payload returns 409. This protects the QUESTION_BANK owner write if Import crashes before its local approval event is recorded. For a newly created EXAM_DRAFT, Import derives a separate exam-service: key for POST /v1/exams; Exam atomically replays the original draft under the same organization, actor, and create payload. The snapshot handoff remains the idempotent PUT /v1/exams/{examId}/question-snapshots.
Import owns a durable approval operation keyed by (jobId, target). Its first claim freezes the request digest, reviewed-output digest, organization, and outbound actor, then persists the Question Bank IDs and the Exam ID after each owner response. A retry uses the same owner keys and resumes from the saved phase; a different reviewed output or request for the same operation returns 409 DOCX_IMPORT_APPROVAL_CONFLICT. A short owner lease returns 409 DOCX_IMPORT_APPROVAL_IN_PROGRESS while another attempt is active. This is a checkpointed retry protocol, not a distributed transaction.
Draft exam approval:
json
{
"target": "EXAM_DRAFT",
"questionMetadata": {
"subjectId": "math",
"gradeLevel": 12
},
"exam": {
"title": "Approved draft",
"subjectId": "math",
"gradeLevel": 12,
"durationMinutes": 45
}
}For EXAM_DRAFT, the request must include either examId or an exam create payload. The service creates the draft when needed and then calls PUT /v1/exams/{examId}/question-snapshots with snapshots built from the QAS output and imported question IDs. A supplied examId must resolve in the same tenant/actor scope. A failed snapshot handoff leaves the persisted Question Bank and newly-created Exam owner records intact, then releases the local operation for retry. Import never deletes owner-service data to compensate an ambiguous remote outcome.
Approval is idempotent for identical requests: the durable operation returns the stored result on retry and approval.completed remains an audit event.
Gateway live approval smoke:
bash
cd go-platform
IMPORT_APPROVAL_LIVE_CONFIRM=approve-native \
IMPORT_APPROVAL_JOB_ID=<completed-job-id> \
IMPORT_APPROVAL_AUTHORIZATION='Bearer <token>' \
IMPORT_APPROVAL_ORGANIZATION_ID=<org-id> \
make test-import-approval-liveUse IMPORT_APPROVAL_BODY_JSON or IMPORT_APPROVAL_BODY_FILE to pass the exact approval payload. The script defaults to {"target":"QUESTION_BANK"} and will not run without the explicit confirmation flag because it performs native target writes.