Skip to content

Import Service API (docx-import-service alias)

Baseline Endpoints

  • POST /v1/import/docx/qas
  • POST /v1/import/docx/qas/stream
  • POST /v1/import/docx/qas/jobs
  • POST /v1/import/mathtype/convert
  • POST /v1/import/mathtype/jobs
  • GET /v1/import/mathtype/jobs/{id}/download
  • POST /v1/import/mathtype/jobs/{id}/reprocess
  • POST /v1/import/docx/docx-fast-jobs
  • POST /v1/import/ocr/jobs
  • POST /v1/import/ocr/jobs/{id}/reprocess
  • GET /v1/import/docx/docx-fast-jobs/{id}/temp-draft
  • POST /v1/import/docx/docx-fast-jobs/{id}/reprocess
  • GET /v1/import/docx/teacher-library
  • GET /v1/import/docx/jobs
  • GET /v1/import/docx/jobs/{id}
  • GET /v1/import/docx/jobs/{id}/status
  • GET /v1/import/docx/jobs/{id}/events
  • GET /v1/import/docx/jobs/events
  • GET /v1/admin/import/docx/queue-summary
  • PATCH /v1/import/docx/jobs/{id}/review
  • POST /v1/import/docx/jobs/{id}/approve
  • GET /healthz
  • GET /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_URL or GO_FORMULA_DOCX_BASE_URL is set, the service streams the upload once to {baseUrl}/v1/import/docx/qas. The adapter reads the QAS envelope rather than the compact simple response so it retains each formula's deterministic conversion provenance. GO_FORMULA_DOCX_TIMEOUT_MS defaults to 180000; 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_URL to http://go-formula-docx:8080 by default and run the existing parser binary as an internal go-formula-docx runtime. 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_CONNECTED instead of silently dropping content.

Media materialization:

  • If DOCUMENT_SERVICE_URL or DOCUMENT_SERVICE_BASE_URL is 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-assets with X-Document-Artifact-Token; production requires DOCUMENT_ARTIFACT_SERVICE_TOKEN and has no shared-token or legacy-presigned fallback.
  • The materializer requires X-Organization-Id for upload ownership and passes X-User-Id as 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}/content with 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_images uses Go Formula renderableImageCount when 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-level stats.equationCount, because the latter also counts source objects repeated in answer/solution rendering. This preserves the corpus metrics of Physics 84 and Math 125 while 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.
  • formulaRefs in the question-bank/exam approval payload carry this provenance in sourceJson, including review flags. Question Bank persists it in question_formula_refs.source_json and question_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/convert is an internal, direct DOCX conversion boundary. It calls the same QAS runtime once, replaces only deterministic mtef-* OLE relationships with OMML, and returns an ephemeral *-word-equation.docx attachment. It reports X-Hoctapaz-MathType-Converted, Skipped, Unresolved, and Warnings response 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_FALLBACK and requires teacher review. A structurally valid DOCX is not a claim of Microsoft Word visual-equivalence for every expression.
  • POST /v1/import/mathtype/convert intentionally remains an ephemeral direct response for diagnostics. The durable path is POST /v1/import/mathtype/jobs: it reopens an organization-scoped DOCX from document-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-artifacts boundary. 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 through GET /v1/import/mathtype/jobs/{id}/download. Processing jobs found after restart fail with MATHTYPE_RECOVERY_REQUIRES_RETRY; POST /v1/import/mathtype/jobs/{id}/reprocess deliberately reads the original source and clears previous artifact metadata before retrying. DOCUMENT_ARTIFACT_SERVICE_TOKEN must match document-service for this workflow.
  • POST /v1/import/docx/jobs/{jobId}/approve rejects 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 new EXAM_DRAFT, it also rejects unsupported workflow or deliveryMode values before calling Question Bank, so those invalid enum values cannot create canonical rows.
  • OCR completion stores editor-facing sourceText plus sections[] and remains review-required. After a teacher saves that review, approval derives canonical questions[] for the Question Bank owner API, including stem/options, reviewed answer labels, solution HTML, source-text spans, and sourceKind=ocr-document. It does not change the editor JSON shape or allow the original unreviewed OCR output to call Question Bank.
  • EXAM_DRAFT approval uses that same reviewed parseResult for 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, and sourceSnapshotJson must 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 opaque sourceAssetId to the native create routes. OCR raw storageKey input 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 in docs/qa/ocr-review-browser-local-postgres.md; it is not default-route, production, batch-mode, or provider-quality promotion evidence.

Classifier baseline:

  • internal/classifier provides 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 styleAnswerHints or docxStyleAnswerHints, the adapter applies them to QAS correct_answer and records answer_evidence=DOCX_STYLE_HINT in source_location.
  • If Go Formula returns zero questions but includes annotatedResult.segments or annotatedSegments, the adapter builds warning-safe QAS questions from those annotated segments and emits GO_FORMULA_DOCX_ANNOTATED_FALLBACK.
  • The native OOXML style-hint pass reads word/document.xml from the uploaded DOCX, detects red/underlined answer runs for Math/English templates and yellow-highlighted answer runs for DGNL templates, then applies hints with DOCX_NATIVE_STYLE_ANSWER_HINT_APPLIED.
  • If the DGNL 102Q runtime payload returns zero QAS questions, the native fallback builds DGNL_SINGLE_CHOICE questions from DOCX A-D option groups, answer-only -> rows, and review-required unlabeled choice rows, then emits DOCX_NATIVE_DGNL_FALLBACK.
  • make test-docx-corpus runs the opt-in Physics/Math/English/DGNL QAS parity checks when GO_FORMULA_DOCX_URL and HOCTAPAZ_DOCX_CORPUS_DIR point to the local runtime/corpus.
  • make test-docx-materialization runs an opt-in Physics 28Q runtime check through live docx-import-service plus document-service, asserts 7 materialized MediaAsset references, and fetches each asset back through /v1/storage/media-assets/{id}/content.
  • DOCX_MATERIALIZATION_SELF_TEST=1 make test-docx-materialization runs 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-capture plus make test-docx-warning-parity compares captured native Physics/Math/English/DGNL response warnings against legacy output/import-audit-20260702/api artifacts. The filtered comparator passes with DOCX_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_URL is configured and reachable, the service uses the pgx-backed docx_import_jobs and docx_import_events tables from services/docx-import-service/migrations/000002_import_jobs.sql, plus payload_ref from services/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:

  • PENDING
  • PROCESSING
  • COMPLETED
  • FAILED

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.queued
  • job.started
  • job.completed
  • job.failed
  • approval.completed
  • auto_approval.completed
  • auto_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-library maps to native GET /v1/import/docx/teacher-library.
  • Legacy POST /api/exam-import/docx-fast-jobs maps to native POST /v1/import/docx/docx-fast-jobs in the non-default gateway create-job example.
  • Legacy GET /api/exam-import/docx-fast-jobs/{id}/temp-draft maps to native GET /v1/import/docx/docx-fast-jobs/{id}/temp-draft only for the materialized read branch in the same non-default gateway example.
  • make test-import-temp-draft-live is 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}/content maps to native GET /v1/import/docx/docx-fast-jobs/{id}/temp-draft/assets/{tempAssetId}/content only for materialized media already referenced by the completed native job.
  • make test-import-temp-asset-live is 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}/reprocess maps to native POST /v1/import/docx/docx-fast-jobs/{id}/reprocess in the same non-default gateway example.
  • make test-import-reprocess-live is 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-live is the opt-in write smoke for the non-default DOCX Fast create route. It requires explicit confirmation, a real uploaded DOCX questionStorageKey, auth/org context, and a gateway running with the native import-create route table.
  • make test-import-create-browser is 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 existing questionStorageKey, then verifies the browser-observed gateway route headers and legacy success envelope.
  • Legacy PATCH /api/exam-import/jobs/{id}/review maps to native PATCH /v1/import/docx/jobs/{id}/review only in the non-default review-save gateway example; all sibling job actions remain legacy.
  • Legacy GET /api/exam-import/jobs/{id} maps to native GET /v1/import/docx/jobs/{id} only in the non-default detail reload gateway example; deeper sibling paths such as /status and /review remain legacy unless separately routed.
  • Legacy /api/exam-import/jobs/{id}/status and /api/imports/{id}/status map to native GET /v1/import/docx/jobs/{id}/status.
  • Legacy /api/exam-import/algorithm-jobs/events maps to native GET /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-live

Use 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.

Go-platform documentation is generated from repository Markdown.