---
id: 10094
title: '[enhancement] Formalize InvalidationDetector interface for updatedAt-blind sync drift detection'
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
  - needs-re-triage
assignees: []
createdAt: '2026-04-19T12:20:05Z'
updatedAt: '2026-06-06T23:49:02Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10094'
author: tobiu
commentsCount: 1
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-06-06T23:49:02Z'
---
# [enhancement] Formalize InvalidationDetector interface for updatedAt-blind sync drift detection

## 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:
```javascript
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).

## Timeline

- 2026-04-19T12:20:07Z @tobiu added the `enhancement` label
- 2026-04-19T12:20:07Z @tobiu added the `ai` label
- 2026-04-19T12:20:07Z @tobiu added the `architecture` label
- 2026-04-19T12:20:44Z @tobiu cross-referenced by #10096
- 2026-04-19T12:29:47Z @tobiu cross-referenced by PR #10098
- 2026-04-19T21:40:56Z @tobiu cross-referenced by #10110
- 2026-04-19T21:53:35Z @tobiu cross-referenced by PR #10111
### @neo-gpt - 2026-06-06T23:48:48Z

**Input from GPT-5 (Codex Desktop):**

> ● [ARCH_ALIGNMENT]
> 
> Ticket-intake classification: **superseded / negative ROI as an implementation lane**.
> 
> V-B-A evidence:
> - #10110 is closed and PR #10111 merged, removing the `detectStaleCommentsCounts` sentinel sweep that made a `CommentTotalsDetector` useful.
> - Current `ai/services/github-workflow/SyncService.mjs` `runFullSync()` no longer invokes any stale-comments sweep.
> - Current `ai/services/github-workflow/sync/IssueSyncer.mjs` derives `commentsTotal` from exhausted `timelineItems` via `#countTimelineComments(issue)`, so the former comment-total sweep target no longer exists.
> - The remaining relationship activity handling is still inline in `pullFromGitHub()`, where fetched timeline events directly collect related issues for `refetchIssuesByNumber()`. That is fetch-path referential-integrity logic, not a second sweep-style detector family.
> - #10110's own Avoided Traps section explicitly called out #10094 as superseded after removing the detector pattern's primary sweep instance.
> 
> Verdict: do not branch or implement `InvalidationDetector`; the abstraction now has no meaningful detector set to unify. Closing this stale backlog item as superseded/not planned.

- 2026-06-06T23:48:55Z @neo-gpt added the `needs-re-triage` label
- 2026-06-06T23:49:03Z @neo-gpt closed this issue

