Stabilize projection ref-cache key so sync never duplicates an issue#35
Merged
Conversation
…, references) Three researched changes for syncing local relations with GitHub issue relations, grounded in research of OpenSpec 1.5.0 and GitHub's GA relationship APIs: - epic-and-subissue-projection (retargeted): project a typed `## Parent` edge onto GitHub native sub-issues (addSubIssue, cross-repo via subIssueUrl); epic = type:epic, no spec; roll up from subIssuesSummary. - issue-dependency-sync (new): directed `## Blocked by`/`## Blocks` edges ↔ GitHub issue dependencies (addBlockedBy/removeBlockedBy). - openspec-references-coordination (new): read OpenSpec references/workset (no new registry) to surface sibling repos + local folders for the two-worktree workflow. Reconcile is genuinely two-way via a gitignored .specsync/ baseline (merge base): edges added on either side propagate to the other, removals on either side propagate too. Binary edges → no ambiguous conflicts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two-way sync created a duplicate GitHub issue (ExoKit #15 -> #16) instead of updating the existing one. Two gaps lined up: - The ref-cache key was unstable: for the same repo, an auto-detected provider keyed "github" while an explicit/pull-constructed one keyed "github:owner/repo", so a ref saved by pull was invisible to a later auto-detect sync (hadRef=false -> Push with existing=nil). - A pull-linked issue was un-rediscoverable: pull never wrote the identity marker into the source issue, so once the cache key was missed, Find could not recover the link and the create path fired. Fix: - GitHubProvider.Name() resolves the concrete target repo once (from -repo or gh's own auto-detect via `gh repo view`) and keys canonically as "github:owner/repo" regardless of how the repo was supplied. Memoized; repoFlag() wire behavior unchanged. - syncOne looks up the canonical key, falls back to the legacy bare "github" key, and re-saves under the canonical key on the next persist (self-healing; existing refs.json caches keep working). - pull upserts the identity marker into the source issue body (idempotent, honors -dry-run) so the link survives cache loss and Find can rediscover it. Implements openspec/changes/stable-projection-ref-key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
Two-way sync created a duplicate GitHub issue instead of updating the existing one. Reproduced against
ExopenGitHub/ExoKit: a change pulled from issue #15 was re-synced and specsync created issue #16 rather than editing #15.Two independent gaps lined up:
GitHubProvider.Name()returned bare"github"when the repo was auto-detected, but"github:owner/repo"when a repo was set (e.g. bypull, which constructs a repo-scoped provider). Sopullcached the ref under"github:owner/repo", while a laterspecsync -slug <change>without-repolooked uprefs["github"], missed it (hadRef=false), and calledPush(existing=nil).Push's duplicate defense falls back toFind, which matches only by the body marker<!-- specsync:change=<slug> -->.pullnever wrote that marker back into the source issue, soFindreturned nil andPushcreated a new issue.Fix
Name()resolves the concrete target repo once — from-repo, or gh's own auto-detect viagh repo view --json nameWithOwner(through the injectedrunfunc) — and keys canonically as"github:owner/repo"regardless of how the repo was supplied. Memoized;repoFlag()wire behavior is unchanged (auto-detect still allowed on the wire).syncOneresolves the canonical key, falls back to the legacy bare"github"key, and re-saves under the canonical key on the next persist. Existingrefs.jsoncaches keep working with no manual edit.pullupserts the identity marker into the source issue body (idempotent — no write if already present; honors-dry-runas a preview). The link is now durable: even if the local cache is lost, a later sync rediscovers the issue viaFindand updates it.Invariant restored: a change pulled from an issue and later pushed (any
-repo/auto-detect combination) updates that same issue and never creates a duplicate.Tests
New
stable_projection_test.gocovers: same-key equivalence (auto-detect vs-repo); legacy-key cache updates + migrates canonically; post-pull cache-loss rediscovery via marker (no duplicate); pull dry-run previews the marker with no GitHub write.go build ./...,go test ./...(60 pass) andgofmton all touched files are clean.OpenSpec
Implements
openspec/changes/stable-projection-ref-key(proposal, tasks,stable-projection-identityspec), included in this PR.🤖 Generated with Claude Code