LearnNewsExamplesServices
Frontmatter
id11573
titleDiscussionSyncer: synced markdown lacks `closed`/`closedAt` despite #11554 — V-B-A post-merge verification gap
stateClosed
labels
bugaimodel-experience
assigneesneo-opus-ada
createdAtMay 18, 2026, 6:41 AM
updatedAtMay 18, 2026, 9:21 AM
githubUrlhttps://github.com/neomjs/neo/issues/11573
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 18, 2026, 9:21 AM

DiscussionSyncer: synced markdown lacks closed/closedAt despite #11554 — V-B-A post-merge verification gap

Closed v13.0.0/archive-v13-0-0-chunk-12 bugaimodel-experience
neo-opus-ada
neo-opus-ada commented on May 18, 2026, 6:41 AM

Context

PR #11555 (commit c70f2672a, merged 2026-05-18 00:07Z) shipped #11554's fix adding closed + closedAt to the Discussion markdown frontmatter at ai/services/github-workflow/sync/DiscussionSyncer.mjs:241-250. Spec at test/playwright/unit/ai/services/github-workflow/DiscussionSyncer.spec.mjs:100 asserts the serialized output carries those fields.

Operator @tobiu V-B-A challenge on 2026-05-18 ~04:30Z: "do we get a markdown file update in case the CLOSED state changes? i do not see MD files with it inside frontmatter. VBA. this was the real ticket scope."

The Problem

Empirical V-B-A confirms operator's observation: 0 of 104 on-disk Discussion markdown files have closed: or closedAt: in frontmatter, including discussion-11557.md which was written by Gemini's chore-sync at commit 709d9c44f AFTER #11554 merged.

$ grep -l "^closed:" resources/content/discussions/chunk-*/*.md | wc -l
0
$ grep -l "^closedAt:" resources/content/discussions/chunk-*/*.md | wc -l
0
$ find resources/content -name "discussion-*.md" | wc -l
104

The code emits the fields correctly under test fixtures (spec passes) and under direct matter.stringify smoke. But every production-synced file post-#11554 still lacks them.

The Architectural Reality

Two contributing factors uncovered during V-B-A:

Factor A — Stale MCP daemon code (operational, likely primary cause):

  • MCP daemons run as long-lived processes in each agent's worktree.
  • They load DiscussionSyncer.mjs at startup and don't auto-reload on code change.
  • If an MCP server was started before #11554 merged, every subsequent sync_all from that agent runs the OLD code without the new frontmatter fields.
  • discussion-11557.md (written by Gemini at 709d9c44f) suggests Gemini's MCP was running pre-#11554 code despite the merge happening ~1h earlier.

Factor B — SyncService drops DiscussionSyncer's metadata mutations (architectural, latent):

  • ai/services/github-workflow/SyncService.mjs:125: await MetadataManager.save(newMetadata) persists newMetadata (returned by IssueSyncer.pullFromGitHub).
  • newMetadata at IssueSyncer.mjs:570-574 is a fresh object with only {issues, pushFailures, lastSync} — it does NOT include discussions or pulls.
  • DiscussionSyncer.syncDiscussions(metadata) and PullRequestSyncer.syncPullRequests(metadata) mutate the OLD metadata (their argument), not newMetadata.
  • Result: metadata.discussions = {} on disk after every sync (verified: 0 entries in resources/content/.sync-metadata.json despite 104 on-disk files). Same for metadata.pulls.
  • This means the discussion-side diff cache is permanently empty → every sync treats every discussion as cache-miss → every sync rewrites every discussion file. Wasteful, not yet a correctness bug, but masks the real cache mechanism.

The two factors compound: Factor A drops the fields at write-time; Factor B prevents the cache from helping a later well-running sync detect the missing-fields and force a rewrite.

The Fix

One concrete prescription, with two coordinated parts:

// SyncService.mjs:125 — BEFORE save, carry over the mutations DiscussionSyncer + PullRequestSyncer made to metadata.
newMetadata.discussions = metadata.discussions;
newMetadata.pulls       = metadata.pulls;
await MetadataManager.save(newMetadata);

Plus a post-sync integrity assertion (new helper or test) that scans a sample of resources/content/discussions/*.md for required frontmatter keys (number, closed, closedAt, etc.) and fails the sync if any are missing — closes the V-B-A gap that allowed #11554 to ship without the production effect.

Plus operational: trigger one full sync_all from a freshly-started MCP server (after merge of this fix) to force rewrite of all 104 existing discussion files with the new frontmatter.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
SyncService.runFullSync metadata persistence This ticket + ADR 0007 compaction taxonomy Preserve metadata.discussions + metadata.pulls across MetadataManager.save call Manual JSON edit JSDoc on runFullSync step 10 Spec test: post-sync metadata.discussions[N] populated for each fetched discussion
Post-sync frontmatter assertion This ticket New helper or test scans discussions/*.md for required keys; fails if missing None New verifyFrontmatterIntegrity.mjs JSDoc Test: missing-field fixture causes assertion fail
Existing 104 markdown files One-time operational re-sync All files gain closed: + closedAt: in frontmatter None — requires fresh sync This ticket `grep -l "^closed:" discussions/chunk-/.md

Acceptance Criteria

  • SyncService.mjs:125 carries metadata.discussions + metadata.pulls onto newMetadata before save.
  • Unit test verifies metadata.discussions[N] survives the full runFullSync cycle (or its orchestrator-equivalent in unit form).
  • Post-sync integrity assertion fails when a synced discussion lacks a required frontmatter key (number, closed, closedAt).
  • After operator/peer-triggered re-sync, all 104 on-disk discussion files have closed: + closedAt: (verified by grep -l count = 104).
  • CI evidence: at least one new spec covering the frontmatter-integrity path.

Out of Scope

  • MCP daemon hot-reload (separate substrate; tracked via #11026 / swarm topology).
  • Pull request markdown integrity (mirrors Discussion problem but separate scope).
  • Release notes integrity (different syncer, different cache).

Avoided Traps / Gold Standards Rejected

  • Trap: Rewrite DiscussionSyncer to write metadata.discussions directly onto newMetadata via a shared reference. Rejected: breaks the explicit IssueSyncer.pullFromGitHub contract that returns a fresh object; better to preserve the post-pull invariant and patch the carry-over at the orchestrator.
  • Trap: Add lint-like "scan all .md files for required frontmatter keys" runner separate from the syncer. Rejected: lints run on PRs, not on sync runs; the integrity check needs to fire on the sync path itself so a future degradation gets caught at sync time, not at next-PR time.
  • Trap: Whole-of-sync rebuild instead of cache-respecting incremental. Rejected: the cache is correct in principle; the bug is in metadata persistence, not in the cache mechanism.

Related

  • Original fix: PR #11555 (commit c70f2672a, #11554 ticket).
  • Empirical anchor: operator V-B-A on 2026-05-18 ~04:30Z confirmed by 0/104 file scan.
  • Substrate discipline: feedback_verify_effect_not_just_success (post-merge effect verification) + feedback_verify_before_assert (multi-layer V-B-A).
  • Related orchestration: SyncService.runFullSync — step 10 metadata save is the choke-point.

Origin Session ID: 0526ccc8-019a-4145-84c2-52b27ef09efd Retrieval Hint: query_raw_memories("DiscussionSyncer closed closedAt frontmatter SyncService newMetadata metadata persistence post-merge verification")

tobiu referenced in commit ad2afdf - "fix(github-workflow): DiscussionSyncer + PullRequestSyncer metadata persistence + frontmatter integrity gate (#11573) (#11574) on May 18, 2026, 9:21 AM
tobiu closed this issue on May 18, 2026, 9:21 AM