ODK Sync Policy
ODK Sync Policy
Purpose
This policy defines how ODK data synchronization interacts with the coding workflow state machine. The core principle is that finalized COD decisions must be protected from inadvertent data changes while still allowing authorized overrides.
Core Principles
ODK is the source of truth for submission content. All submissions present in ODK are stored in DigitVA — including those with refused or missing consent. This ensures complete auditability and allows consent corrections made in ODK to be picked up automatically on the next sync.
Coder-finalized submissions have protected status that prevents automatic destructive refresh without explicit authorization.
Consent determines workflow routing, not storage. Submissions without valid explicit consent are stored but never enter the coding workflow.
Per-Form Sync Pipeline
Sync must complete work form-by-form rather than treating SmartVA as one global second pass after all forms are fetched.
Current required order for each form:
- fetch thin ODK rows for that form
- thin-upsert those submissions locally
- enrich the changed submissions for that same form into the canonical stored payload
- sync attachments for that form's changed submissions
- move those submissions from
attachment_sync_pendingtosmartva_pending - run SmartVA for that form
- only then move on to the next form in the sync loop
Policy reasons:
- each form reaches a logical completion point before the next form starts
- progress logging stays easy to read and troubleshoot
- coding remains blocked until enrichment, attachments, and SmartVA are complete for the current payload
- partial form failure does not require waiting for a whole-run global SmartVA phase to understand what happened
Backfill repair path
The admin sync dashboard may expose a form-scoped ODK-backed backfill trigger. That trigger is a repair path, not a separate data source.
Policy baseline:
- a form
Backfillis distinct from aForce-resync - the backfill should be scoped narrowly to the selected project/site/form
- the backfill may repair thin submission data, metadata enrichment, attachment sync, and SmartVA follow-through together for that form
- the backfill should fetch ODK instance IDs for the selected form, fetch only missing thin submissions, and then queue repair only for submissions that are metadata-incomplete, attachment-incomplete, or missing current-payload SmartVA
- before ordinary metadata, attachment, or SmartVA repair continues for an already-local submission, the backfill must revalidate the current ODK payload for that submission
- if that revalidation detects a protected upstream payload change, the
submission must follow the existing
finalized_upstream_changedpath and must not continue through ordinary attachment or SmartVA repair against the stale active payload - a
Force-resyncmay still bypass delta checks and redownload all form rows, but that behavior belongs to the explicit formForce-resyncaction, not theBackfillaction - legacy attachment repair may remain a distinct admin trigger, but it should feed candidate submissions through the same canonical per-submission repair engine rather than maintaining a second attachment-only repair implementation
- backfill actions should be logged with clear form-scoped progress so an operator can tell whether the repair is fetching data, enriching metadata, syncing attachments, or advancing workflow
CLI repair baseline:
flask payload-backfill enrichis a metadata-enrichment-first repair path for active payload versions- after the CLI selects candidate submissions, it should reuse the same per-submission current-payload repair engine used by other repair entrypoints
- the shared per-submission current-payload repair engine is the canonical repair baseline for new entrypoints
- after metadata writes, it may run attachment sync for the same candidate submissions, reusing ETag conditional downloads and existing local file cache
- AMR attachments should continue to convert to MP3 during attachment sync with existing conversion policy and tooling
- the repair path may then advance eligible workflow states from
attachment_sync_pendingtosmartva_pending, and toready_for_codingwhen current-payload SmartVA is present - it must emit explicit per-submission stage logs for enrichment and SmartVA follow-through
- after metadata writes complete, it must check whether each candidate
submission has an active SmartVA projection for the current
active_payload_version_id - if current-payload SmartVA is missing and the submission is eligible under SmartVA policy protections, it may trigger per-submission SmartVA generation
Attachment Quarantine Baseline
Operational attachment cleanup must preserve recoverability.
Policy baseline:
- orphaned attachment files should be quarantined under a
.orphaneddirectory inside the relevantmediatree - cleanup must be reversible without requiring DB surgery or manual file reconstruction
- cleanup scans must ignore already quarantined
.orphanedsubtrees so quarantined files do not keep reappearing as active orphaned media - cleanup tools must not overwrite existing quarantined files when a name collision occurs; they should keep both copies and preserve the original source path in the report
On-Open Current-Payload Repair
Coding-route page opens may trigger a synchronous single-submission repair, but only when the current active payload still has local completeness gaps.
Policy baseline:
- on-open repair is a scoped reuse of the existing repair pipeline, not a separate repair model
- it must first revalidate the current ODK payload for that submission before ordinary attachment or SmartVA repair continues
- it may repair metadata, attachments, legacy attachment-row migration, and current-payload SmartVA for that one submission
- it must not run for every coding-page request indiscriminately; it is gap triggered only
- if revalidation detects a protected upstream payload change, the submission
must follow the existing
finalized_upstream_changedhandling and ordinary on-open attachment or SmartVA repair must stop - on-open repair is allowed to increase page-open latency for genuinely incomplete submissions, but it should avoid unnecessary remote work when the local current payload is already complete
Payload Versioning Baseline
The sync model must distinguish the current live submission row from the specific normalized ODK payload version that produced it.
Canonical terms:
source submission: the ODK submission identified byva_sidpayload version: one concrete normalized ODK payload for thatva_sidactive payload version: the payload version currently accepted as the live basis for workflow, SmartVA, and human codingpending upstream payload version: a newer changed ODK payload detected while the submission is in a protected finalized state; it is not active until an authorized accept actionsuperseded payload version: an older payload version that was previously active but has been replaced
Policy intent:
- unchanged syncs do not create a new logical payload version
- changed syncs do create a new logical payload version
- SmartVA and human COD artifacts should attach to a payload version, not only
to
va_sid va_submissionsmay remain the current active summary row, but payload lineage should be preserved explicitly
Submission Identity Baseline
DigitVA submission identity is anchored on the stable ODK submission KEY
(__id in OData), not on ODK instanceID.
Current rule:
- normalized ODK fetch sets
KEYfrom__id - normalized ODK fetch computes
sidas{KEY}-{form_id.lower()} instanceIDis treated as operational metadata that may vary across edited versions of the same ODK submissionva_uniqueid/ displayedVA Form IDis a business identifier for users and must not be treated as the canonical technical sync identity
Policy implications:
- sync matching and local submission identity must remain anchored to
KEY instanceIDchanges alone do not imply a new DigitVA submission- reviewer-facing upstream-change diffs should not treat
instanceIDchurn as a substantive data change when the underlying ODKKEYis unchanged
Changed vs unchanged payload
A submission is considered unchanged only when the normalized synced payload is semantically identical to the current active payload version.
This should be determined by a canonical payload fingerprint or equivalent versioning rule, not only by ODK timestamps.
Practical implication:
- the current agreed rule is to fingerprint the full canonical normalized ODK payload JSON
- volatile metadata and derived helper fields do not count as substantive
business-payload change; examples include
updatedAt, attachment counters, ODK review comments, device metadata, and derived age-band helper fields - numeric and string-equivalent scalar values compare equal after
normalization; for example
10.0and"10"must not create a new logical payload version on their own - materially different normalized payload should be treated as a new payload version even if workflow routing happens later
Canonical payload enrichment
The canonical stored submission payload remains OData-first, but selected operational metadata is now enriched before persistence so payload versions and upstream-change review use one stable contract.
Current required canonical stored fields include:
- OData form-answer payload
FormVersionDeviceIDSubmitterIDinstanceIDReviewStateinstanceNameAttachmentsExpectedAttachmentsPresent
Current source precedence:
- OData:
canonical form-answer payload plus mirrored ODK fields such as
ReviewStateandinstanceName - submission XML:
FormVersion,DeviceID - submission metadata endpoint:
SubmitterID,instanceID, and fallback review metadata - attachments endpoint:
AttachmentsExpected,AttachmentsPresent
Policy boundary:
- this enrichment is part of the canonical stored payload for sync writes and payload-version lineage
- this does not replace OData as the primary payload source
- attachment inventory details such as
audit.csvpresence may be used operationally, but are not required as coder-facing canonical payload fields
How To Identify The Current Artifact For A SID
Use these rules when a submission has history across syncs, recodes, or protected upstream review.
Current VA data payload
- read
va_submissions.active_payload_version_id - then read the matching row in
va_submission_payload_versions - that payload row is the current accepted ODK data for the
va_sid
Current SmartVA payload
- find the active
va_smartva_resultsrow for theva_sid - it must have
payload_version_id = va_submissions.active_payload_version_id - if it does not, the SmartVA projection is stale and needs repair
Current finalized ICD10/COD
- authoritative ICD/COD comes from final-COD authority resolution
- coder final COD and reviewer final COD rows are both payload-version aware
- only rows linked to the current active payload should be treated as current
Current reviewer-owned artifacts
- reviewer artifacts are downstream authority-chain artifacts
- if reviewer-owned final COD or other reviewer-owned payload-bound artifacts exist, they follow the same preserve/deactivate decision as coder-owned authoritative artifacts for the same SID
- reviewer review/NQA is a payload-bound supporting artifact in reviewer flows
Accept And Recodedeactivates both coder and reviewer authoritative artifacts, and also deactivates reviewer supporting review artifacts, because the accepted upstream payload invalidates the existing coding conclusion chainKeep Current ICD Decisionpreserves both coder and reviewer authoritative artifacts, and also preserves reviewer supporting review artifacts, if present, because the explicit policy decision is that the existing ICD/COD conclusion remains valid for the promoted payload
Current coder NQA
- find the active row in
va_narrative_assessments - it must match both the coder and the submission's
active_payload_version_id
Current Social Autopsy
- find the active row in
va_social_autopsy_analyses - it must match both the coder and the submission's
active_payload_version_id
Simple Upstream Review Examples
Example: accept and recode
- Current payload is
P1 - Protected sync creates pending payload
P2 - Data manager chooses
Accept And Recode - DigitVA promotes
P2 - Coder final COD, reviewer final COD if present, current NQA, current Social Autopsy, and current SmartVA are no longer treated as current artifacts for coding
- SmartVA reruns and coding starts again on
P2
Example: keep current ICD decision
- Current payload is
P1 - Protected sync creates pending payload
P2 - Data manager chooses
Keep Current ICD Decision - DigitVA promotes
P2 - Coder final COD remains authoritative, and reviewer final COD remains authoritative too if a reviewer-owned final COD exists
- SmartVA, coder NQA, and Social Autopsy are rebound to
P2instead of being regenerated
Consent Routing
Consent is evaluated on every upsert (insert and update). The Id10013 field is the consent field.
Consent validity
Consent is considered valid when:
- the field is present and non-empty, and
- the value is not
"no"
Examples: "yes", "telephonic_consent" → valid. "no", "", null → refused.
Workflow state assignment
| Condition | Workflow state set |
|---|---|
| Consent valid | attachment_sync_pending immediately after sync; after attachment syncing for the current payload completes, the submission advances to smartva_pending |
Consent = "no" |
consent_refused |
| Consent missing / empty | consent_refused |
consent_refused state
- Submissions are stored in full — ODK data, attachments, and metadata are all synced normally.
- The submission never enters the coding queue.
- SmartVA is not run on
consent_refusedsubmissions. - If consent is corrected in ODK Central, the next sync automatically re-evaluates and transitions the submission into the coding-eligibility path.
Current runtime:
attachment_sync_pending -> smartva_pending -> ready_for_codingDesired target:attachment_sync_pending -> smartva_pending -> ready_for_coding - Data managers can see and filter
consent_refusedsubmissions and view their count on the dashboard.
Attachment Sync Gate
Current baseline:
- newly inserted or payload-changed consent-valid submissions first enter
attachment_sync_pending - attachment downloads may run in chunked Celery subtasks per form
- only after all attachment batches for that form finish does the workflow
advance to
smartva_pending - only after SmartVA generate/regenerate/failure-recording may the submission
become
ready_for_coding
This keeps submissions non-allocatable while attachment syncing is still in progress, even when large forms are split across many background attachment batches.
Workflow State Guards
Protected States
The following workflow states are protected from automatic ODK data refresh:
coder_finalized— Final COD has been submitted and is authoritativefinalized_upstream_changed— Current state key for finalized cases whose ODK data changed after finalizationreviewer_eligible— Post-24-hour resting state before any optional reviewer codingreviewer_coding_in_progress— Reviewer has an active mid-session allocation; automatic re-routing would orphan the allocation; requires DM accept/rejectreviewer_finalized— Reviewer-owned final COD is now authoritativeclosed— Legacy compatibility state only; if such rows exist they remain protected
Current naming:
- current persisted key:
finalized_upstream_changed - legacy migrated key:
revoked_va_data_changed - UI label:
Finalized - ODK Data Changed
Current implementation note:
- runtime now writes
reviewer_eligibleinstead ofclosedafter coder recode-window expiry - sync should still treat
closedas protected if such legacy rows exist
consent_refused is not protected — ODK updates flow through freely so that consent corrections are picked up automatically.
Non-Protected States
These states allow normal ODK sync behavior (consent re-evaluated on each update):
consent_refused— ODK updates flow freely; consent corrections automatically re-route the submission intosmartva_pendingscreening_pendingsmartva_pendingready_for_codingcoding_in_progresspartial_coding_savedcoder_step1_savednot_codeable_by_coder— ODK data change deactivates the exclusion artifact and re-routes tosmartva_pending; coder may re-exclude after reviewnot_codeable_by_data_manager— ODK data change deactivates the DM exclusion artifact and re-routes tosmartva_pending; DM may re-exclude after review
Allocations During Sync
A sync releases an allocation only for a case whose ODK payload changed in
that run and whose state is non-protected: the case's own coding allocation
is released with its first-pass artifacts
(va_allocation_released_during_datasync; the release is
release_coding_for_changed_payload in app/services/coding_release_service.py,
shared with an interviewer's revision, which audits interviewer_revision
instead). A protected case keeps its
allocations and goes through the upstream-change path. A case whose payload
did not change keeps any coder or reviewer session untouched, including a
metadata-only refresh. Abandoned allocations are released only by the
scheduled stale-allocation cleanup. Until 2026-09-27 every sync also
released every active allocation in the system
(va_allocation_deletion_during_datasync); that global release was removed
because it ended live sessions on unchanged cases.
Sync Operations
Single Submission Refresh (fetch_single)
fetch_single(va_sid, force=False)
| Workflow State | force=False | force=True |
|---|---|---|
| Non-protected | Fetch from ODK, upsert | Fetch from ODK, upsert |
| Protected | SKIP — return reason | Fetch from ODK, upsert |
Authorization for force=True: Admin only
Form-Level Sync (fetch_form_submissions)
fetch_form_submissions(form_id, force=False)
For each submission in the form:
| Workflow State | ODK Data Changed | force=False | force=True |
|---|---|---|---|
| Non-protected | Yes | Fetch, upsert, run SmartVA | Fetch, upsert, run SmartVA |
| Non-protected | No | Skip (no change) | Skip (no change) |
| Protected | Yes | Mark as finalized_upstream_changed, preserve workflow artifacts, require manual resolution |
Admin-only explicit override path required |
| Protected | No | Skip | Skip |
Bulk Sync (fetch_all)
Same behavior as form-level sync, applied across all active forms.
Upstream Data Change Handling
When ODK data changes for a coder_finalized submission:
Current Implemented Baseline
The current runtime behavior is partially aligned with this policy:
- Protected submissions are not auto-reset to
ready_for_coding - Existing coding artifacts are not destroyed on protected-state ODK update
- The submission transitions to
finalized_upstream_changed - Audit logging is written for the state transition
- Durable upstream-change records preserve prior and incoming payload snapshots for the upstream-review decision path
- Data-manager and admin review surfaces expose a structured changed-fields diff derived from the normalized payload, not raw JSON inequality
Review Surface Baseline
Pending upstream-change review must be available through one shared backend contract.
Required surfaces:
- JSON API:
/api/v1/data-management/submissions/<va_sid>/upstream-change-details - data-manager detail-page
View Changesmodal on/data-management/view/<va_sid> - dashboard
View Changesmodal forfinalized_upstream_changedrows
The changed-fields presentation must:
- separate data changes from metadata changes
- separate formatting-only changes from substantive data changes
- show field labels when form mapping metadata exists
- keep stable
field_idvalues in the payload even when no label mapping exists
Current terminology:
Data ChangesMetadata ChangesFormatting-Only Changes
Implementation Status
All items below are fully implemented as of 2026-04-01:
- Runtime sync writes active and pending payload versions; protected upstream review outcomes now promote the pending payload version on both branches
- Coder and reviewer final COD are each linked to the
payload_version_idthat was active when they were submitted - SmartVA runs are bound to payload version via
VaSmartvaFormRun/VaSmartvaRunlinkage - Legacy
revoked_va_data_changedrows migrated tofinalized_upstream_changed
Required Behavior
When upstream data changes for a finalized submission:
- Do NOT destroy workflow artifacts
- Do NOT reset workflow state automatically
- Transition to the finalized-upstream-change state
Current key:
finalized_upstream_changedLegacy migrated key:revoked_va_data_changed - Preserve historical COD and its authority linkage relative to the current active payload version
- Preserve historical VA data and incoming VA data as distinct payload versions or version-equivalent snapshots
- Create notification for data managers/admins
- Log audit trail with reason
upstream_data_changed - Do not activate the new payload version automatically while the case is awaiting upstream-review resolution
State Transition
coder_finalized --[ODK data changed]--> finalized_upstream_changed
This state indicates:
- The submission was previously finalized
- Upstream ODK data has changed since finalization
- The previous active payload version and authoritative COD are preserved but may need review
- A newer upstream payload version has been detected but is not yet the active coding basis
- Manual intervention required to either:
- Accept and recode: activate the new payload version for SmartVA/coding
- Keep current ICD decision: activate the new payload version while preserving the current authoritative COD
Historical Data Preservation
When transitioning to the finalized-upstream-change state:
| What | How |
|---|---|
| Final COD | Preserve explicit linkage to the previously authoritative final assessment for the current active payload version |
| VA data snapshot | Preserve both prior active payload and incoming changed payload; current code does this through upstream-change snapshots, while additive payload-version schema now exists for the planned general lineage cutover |
| Audit trail | VaSubmissionsAuditlog entries with full context |
| Notification | Created for data managers and admins |
Payload-Version Routing Rules
Target routing rules:
| Situation | Payload version action | Workflow action |
|---|---|---|
| First sync | Create initial active payload version | Route by consent to consent_refused or attachment_sync_pending; then advance to smartva_pending after attachment sync finishes |
| Unchanged sync | No new payload version | No SmartVA rerun; preserve current workflow unless other sync-issue handling applies |
| Changed sync on non-protected state | Create new active payload version | Route to attachment_sync_pending; then smartva_pending and regenerate SmartVA for the new payload after attachment sync finishes |
| Changed sync on protected finalized state | Create pending upstream payload version | Route to finalized_upstream_changed |
| Accept And Recode | Promote pending payload version to active and update va_submissions from it |
Route to smartva_pending and regenerate SmartVA for the new payload |
| Keep Current ICD Decision | Promote pending payload version to active and update va_submissions from it |
Return to prior finalized path while preserving COD artifacts and rebinding the preserved SmartVA projection to the new payload |
Notification Requirements
When a finalized submission's upstream data changes:
Immediate notification to:
- Data managers for the project/site
- System admins
Notification content:
- Submission identifier (va_sid, instance name)
- Project/site/form information
- Timestamp of change detection
- Previous workflow state
- Action required (review and decide)
Dashboard visibility:
- Submissions in the finalized-upstream-change state appear in a dedicated queue
- Count shown on data manager dashboard
- Filterable in submission lists
Authorization Matrix
| Operation | Coder | Data Manager | Admin |
|---|---|---|---|
| Single submission refresh (non-protected) | No | Yes | Yes |
| Single submission refresh (protected, force) | No | No | Yes |
| Form sync | No | Yes | Yes |
| Form sync (force on protected) | No | No | Yes |
| Bulk sync | No | No | Yes |
| Accept And Recode | No | Yes | Yes |
| Keep Current ICD Decision | No | Yes | Yes |
Desired State Snapshot
coder_finalized -- ODK data changed during sync --> finalized_upstream_changed
UI: Finalized - ODK Data Changed
finalized_upstream_changed -- data manager or admin accepts upstream change --> smartva_pending
attachment_sync_pending --- attachment sync finished -----------------------> smartva_pending
smartva_pending ----------- SmartVA generated / regenerated / recorded failure --> ready_for_coding
finalized_upstream_changed -- data manager or admin keeps current ICD decision --> prior finalized state
coder_finalized -- recode window expires automatically --> reviewer_eligible
coder_finalized -- admin override final COD -----------> ready_for_coding
Attachment and SmartVA gate note:
- any path that introduces a newly synced ODK payload must first pass through
attachment_sync_pending - any path that introduces a new or changed payload into the coding queue must
then pass through
smartva_pending - that includes initial sync eligibility and accept-upstream-change
- same-payload returns such as timeout cleanup, demo cleanup, or admin override do not require a SmartVA rerun
- active SmartVA projection rows must always match the submission's current active payload version
- prior SmartVA runs and likelihood rows remain durable history even after the projection row is superseded
Service Architecture
ODKSyncService
├── fetch_single(va_sid, force=False) -> SyncResult
├── fetch_form_submissions(form_id, force=False) -> FormSyncResult
├── fetch_all(project_id=None, force=False) -> BulkSyncResult
├── accept_upstream_change(va_sid) -> AcceptResult
└── reject_upstream_change(va_sid) -> KeepCurrentIcdResult
SyncResult:
├── status: "synced" | "skipped_protected" | "error"
├── reason: str (when skipped)
├── submission: VaSubmissions (when synced)
└── audit_entries: list[VaSubmissionsAuditlog]
Connection Guard and Pacing
All outbound ODK Central calls pass through a shared connection guard
(app/services/odk_connection_guard_service.py). The guard enforces
per-connection pacing and cooldown using state persisted in mas_odk_connections.
Pacing
Every ODK call reserves a slot via reserve_odk_request_slot(), which uses a
DB row-level lock on the connection row to enforce a minimum interval between
consecutive calls.
| Config key | Default | Meaning |
|---|---|---|
ODK_CONNECTION_MIN_REQUEST_INTERVAL_SECONDS |
0.5 |
Minimum seconds between consecutive calls on the same connection. Set to 0 to disable pacing. |
If a call arrives before the interval has elapsed, the caller sleeps the remaining wait before proceeding.
Failure tracking and cooldown
Each retryable ODK failure increments consecutive_failure_count on the
connection row. When the count reaches the threshold, cooldown_until is set
to now + cooldown_seconds. While cooldown is active, any guarded_odk_call
for that connection raises OdkConnectionCooldownError immediately without
touching ODK.
A single successful ODK call resets consecutive_failure_count to zero and
clears cooldown_until.
| Config key | Default | Meaning |
|---|---|---|
ODK_CONNECTION_FAILURE_THRESHOLD |
3 |
Consecutive retryable failures before cooldown activates |
ODK_CONNECTION_FAILURE_COOLDOWN_SECONDS |
300 |
Cooldown duration in seconds (5 minutes) |
Retryable errors
The following are treated as retryable connectivity failures that count toward the threshold:
requests.exceptions.ConnectTimeoutrequests.exceptions.ConnectionErrorrequests.exceptions.Timeout- HTTP 401 or 403 responses (auth failure / token expiry)
- Exceptions whose string representation contains any of:
ConnectTimeout,ConnectionError,Max retries exceeded,timed out,HTTPSConnectionPool,Unauthorized,Forbidden,token,expired,auth
OdkConnectionCooldownError itself is not retryable — it is a guard signal,
not a connectivity error, and must propagate immediately.
Sync loop behaviour on cooldown
When OdkConnectionCooldownError is raised during a form's sync:
- The form is logged as SKIPPED at
WARNINGlevel (notERROR) - The connection ID is added to
connections_in_cooldownfor this run - All subsequent forms sharing the same connection are preemptively skipped at the top of the loop without making any ODK calls
- Forms on other connections are unaffected and continue running
- Cooldown-skipped forms are reported separately in the Phase 1 summary
(
cooldown_skipped_forms) and are not counted as failures
A non-cooldown exception (data error, transient HTTP failure below threshold)
continues to go to the FAILED path with full error logging and does not
preemptively skip other forms.
Operator visibility
Connection guard state is surfaced in the admin dashboard at three points:
- Sync dashboard — shows cooldown status and remaining cooldown time for each connection involved in the current or most recent sync run
- ODK Connections panel — shows
consecutive_failure_count, active cooldown flag, and last failure message per connection - Project forms panel — shows a cooldown warning banner when a live ODK action is attempted and the connection is in cooldown
The serialize_connection_guard_state() helper in the guard service produces
the UI-safe dict consumed by all three.
Comments and updatedAt
Adding a comment or changing reviewState on a submission via
client.submissions.review() does not change __system/updatedAt in ODK
Central. ODK only bumps updatedAt for data-XML edits. This means:
- DigitVA's writeback calls (
mark_submission_needs_revision,post_dm_rejection_comment) are invisible to the delta check(submissionDate gt T or updatedAt gt T) - No spurious re-syncs are triggered by our own review state writes