Acceptance Criteria: Instant Data-Repo → Main-Repo Sync
Feature ID: data-repo-instant-sync
Spec: ./spec.md
Status: Draft
Last updated: 2026-05-16
Core behaviour
AC-1: Webhook-driven sync (App installed)
- Editing
categories.ymlon the data repo and committing to default branch triggers a sync within 30 seconds (debounce window). - The main repo's
README.mdreflects the new category after the sync completes. - An activity row of type
data-sync.successappears withsource: webhook, correctbeforeSha/afterSha, andfilesChanged >= 1. - Five commits to the data repo within 10 seconds coalesce into a single sync run (one
.successrow,afterSha = HEAD of last commit).
AC-2: Poller-driven sync (App not installed)
- On a Work with
githubAppInstalled = falseandsyncIntervalMinutes = 5, editing a markdown file in the data repo via a manualgit pushresults in a sync within ≤ 5 minutes. - Activity row
data-sync.successrecordssource: poll. - If no commits are made for 24h, only 24
data-sync.skipped reason=no-changesrows appear (rate-limited to 1 per hour), not 288.
AC-3: Mutex with generation pipeline
- When a generation pipeline run is
RUNNING, a concurrent sync attempt emitsdata-sync.skipped reason=generation-in-progressand does NOT callMarkdownGeneratorService.syncFromDataRepo(). - After the generation pipeline completes, the next webhook delivery (Path A) or next poll tick (Path B) detects the SHA delta and successfully runs the deferred sync.
- When two sync runs are triggered within the same second, only one acquires the lock; the other emits
.skipped reason=sync-in-progress.
AC-4: Lock TTL recovery
- If
dataRepoSyncTaskcrashes with the lock held (simulated viakill -9), a new sync attempt 5 minutes later acquires the lock cleanly (no stuck Work). - The dispatcher (
WorkScheduleDispatcherService.dispatchDue) skips the locked Work and resumes scheduling for it once the lock expires.
Render correctness
AC-5: Render parity with full pipeline
- For a Work with no item changes (only category/tag changes), the resulting
README.mdproduced bysyncFromDataRepo()is byte-identical to the README produced by a fullWorkScheduleDispatcherTaskrun. -
details/<slug>.mdfiles are written exactly as they would be by the full pipeline. - No
items/regeneration occurs — verified by absence of AI provider calls in test logs.
AC-6: Idempotency
- Running
syncFromDataRepotwice in a row against the same data-repo SHA producesfilesChanged: 0on the second run. - The second run still emits an activity row (
.successwithfilesChanged: 0) so users can see the sync attempt happened.
Failure modes
AC-7: Data repo unreachable
- If the data repo returns 404 / 403 from GitHub, the sync emits
data-sync.failedwitherrorClass: 'data-repo-unreachable'anderrorTailcontaining the last 200 chars of stderr. - The lock is released; subsequent runs are not blocked.
- The Work's
lastSyncedDataRepoShais not updated.
AC-8: Main repo push rejected
- If the main repo push is rejected (branch protection / force-push refused / merge conflict from a parallel manual edit), the sync emits
data-sync.failedwitherrorClass: 'main-repo-push-rejected'. - On the next tick the sync retries; if the underlying conflict resolves naturally, the retry succeeds and emits
.success.
AC-9: Webhook signature invalid
- A
pushpayload with an invalidx-hub-signature-256returns 401 from the webhook controller, does not emit any activity row, and does not touch the lock or queue.