LearnNewsExamplesServices
Frontmatter
id10094
title[enhancement] Formalize InvalidationDetector interface for updatedAt-blind sync drift detection
stateClosed
labels
enhancementaiarchitectureneeds-re-triage
assignees[]
createdAtApr 19, 2026, 2:20 PM
updatedAtJun 7, 2026, 1:49 AM
githubUrlhttps://github.com/neomjs/neo/issues/10094
authortobiu
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 7, 2026, 1:49 AM

[enhancement] Formalize InvalidationDetector interface for updatedAt-blind sync drift detection

Closed v13.0.0/archive-v13-0-0-chunk-4 enhancementaiarchitectureneeds-re-triage
tobiu
tobiu commented on Apr 19, 2026, 2:20 PM

Context

Three PRs in the sync architecture each solve the same underlying problem — GitHub's issue.updatedAt does not bump for certain state transitions — with bespoke, one-off mechanisms:

  1. #7948 (Nov 2025) — SubIssueAddedEvent, BlockedByAddedEvent, etc. relationship-event timeline scan inside pullFromGitHub.
  2. #10090 / PR #10091 (Apr 2026) — timelineItems truncation; fixed by GraphQL pagination via pageInfo.hasNextPage inside #exhaustTimelineItems.
  3. #10092 / PR #10093 (Apr 2026) — comment deletions; fixed by a totals-sentinel batch query inside detectStaleCommentsCounts.

Each has: (a) a detection primitive living in IssueSyncer as a bespoke method, (b) a direct call from runFullSync, (c) and the last two converged on a shared recovery primitive (refetchIssuesByNumber). A fourth class WILL emerge — milestone metadata, reaction counts, review-thread state on PRs are all candidate blind spots. Each future fix will add another bespoke branch to runFullSync unless this pattern is formalized.

The Ask

Introduce a pluggable InvalidationDetector interface. Each detector:

  • Implements detect(metadata): Promise<{stale: number[], seeded: number, errors: Array}>
  • Is registered in a central array consumed by runFullSync after the delta pull
  • Feeds its stale set into the shared refetchIssuesByNumber recovery primitive

runFullSync becomes:

const detectors = [RelationshipDetector, CommentTotalsDetector, /* future */];
const allStale  = new Set();
for (const detector of detectors) {
    const result = await detector.detect(newMetadata);
    result.stale.forEach(n => allStale.add(n));
    mergeStats(detectorStats, result);
}
if (allStale.size > 0) await IssueSyncer.refetchIssuesByNumber([...allStale], newMetadata);

Acceptance Criteria

  • ai/mcp/server/github-workflow/services/sync/detectors/Base.mjs — abstract class with detect(metadata) contract
  • Extract the two existing sweep-style mechanisms into discrete detector classes: RelationshipDetector, CommentTotalsDetector (timelineItems pagination stays inside #exhaustTimelineItems since it's intrinsic to the fetch path — not a sweep)
  • runFullSync iterates a registered detector list instead of explicitly calling each
  • Playwright spec for the registration + iteration pattern with a mock detector
  • Each detector's doc-header explicitly names its updatedAt blind spot + the GitHub API behavior that causes it

Avoided Trap

Making TimelineTruncationDetector a first-class detector would be wrong. It's intrinsic to the fetch path (the pagination happens when we DO fetch, not as a sweep). Keep it inside #exhaustTimelineItems; the detector interface is for sweep-style deferred detection only.

Origin Session ID

d9eb5e76-5430-45f7-b3ea-8600664d28f9

Related: #10090 ( timelineItems pagination), #10092 (comment-totals sentinel), #7948 (relationship events).

tobiu added the enhancement label on Apr 19, 2026, 2:20 PM
tobiu added the ai label on Apr 19, 2026, 2:20 PM
tobiu added the architecture label on Apr 19, 2026, 2:20 PM
tobiu cross-referenced by #10096 on Apr 19, 2026, 2:20 PM
tobiu cross-referenced by PR #10098 on Apr 19, 2026, 2:29 PM
tobiu cross-referenced by #10110 on Apr 19, 2026, 11:40 PM
tobiu cross-referenced by PR #10111 on Apr 19, 2026, 11:53 PM