From 3b9d87fcdee692b9f3d40d33be320af8ac05e4b0 Mon Sep 17 00:00:00 2001 From: Thomas Jung Date: Tue, 26 May 2026 12:48:50 -0600 Subject: [PATCH] fix(sync): clean up stale context.expanded.md when markers removed When a pack's context.md loses all its markers, runMarkerExpansion previously returned early without touching the existing context.expanded.md. Because the loader prefers context.expanded.md over context.md, sync+inject would keep serving the stale fetched content indefinitely. The fix deletes the previously-written context.expanded.md for any pack that no longer has markers, so the loader falls back to the fresh context.md. --- CLAUDE.md | 2 +- cmd/sync.go | 17 ++++++++++++++++ cmd/sync_test.go | 41 +++++++++++++++++++++++++++++++++++++++ docs/content-authoring.md | 2 ++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 cmd/sync_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 37b46c7..a293a30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,7 +86,7 @@ Profiles ([content/profiles/](content/profiles/)) are YAML files that tag which **Independent sync categories** run in parallel after the archive fetch: `events`, `youtube`, `news`, `discovery`, `tutorials`, `learning`. Each has its own TTL in `config.yaml`. The `news` category (default 2h) uses `runNewsFetch` which tries RSS with retry → YouTube API v3 → baseline file fallback, then caches to `/news/news-cache.json`. -**Phase 2 — Dynamic Content Expansion:** After the zip fetch, `sync` scans each `context.md` for `` markers via `ScanMarkers()`, fetches remote content in parallel (Bubbletea progress UI), then writes `context.expanded.md` alongside `context.md`. `inject` prefers `context.expanded.md` when present. Marker authoring details: [docs/content-authoring.md](docs/content-authoring.md). +**Phase 2 — Dynamic Content Expansion:** After the zip fetch, `sync` scans each `context.md` for `` markers via `ScanMarkers()`, fetches remote content in parallel (Bubbletea progress UI), then writes `context.expanded.md` alongside `context.md`. `inject` prefers `context.expanded.md` when present. When a pack loses all its markers between syncs, `runMarkerExpansion` deletes the previously-written `context.expanded.md` so the loader falls back to the fresh `context.md` instead of serving stale fetched content. Marker authoring details: [docs/content-authoring.md](docs/content-authoring.md). ### Discovery Center diff --git a/cmd/sync.go b/cmd/sync.go index f52bf79..414d1c5 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -448,6 +448,17 @@ func runSyncPlain(plan *syncPlan, out io.Writer) error { return nil } +// removeStaleExpansion deletes packDir/context.expanded.md if it exists. +// Used when a pack no longer has sync:fetch markers — without this the loader +// would keep preferring the stale expansion over the fresh context.md. +func removeStaleExpansion(packDir string) error { + path := filepath.Join(packDir, "context.expanded.md") + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + // runMarkerExpansion scans all official-layer packs for sync:fetch markers, // fetches them in parallel, and writes context.expanded.md alongside each context.md. // When p is non-nil, it sends SetMarkersMsg and MarkerDoneMsg to the Bubbletea program. @@ -488,6 +499,12 @@ func runMarkerExpansion(officialCache string, engine *sapSync.Engine, p *tea.Pro if hasMarkers { packContexts[packID] = contextContent allMarkers = append(allMarkers, markers...) + } else { + // Pack lost its markers since the previous sync — drop the stale + // expanded file so the loader falls back to the fresh context.md. + if err := removeStaleExpansion(filepath.Join(packsDir, packID)); err != nil { + return fmt.Errorf("clean up stale expansion for %s: %w", packID, err) + } } } diff --git a/cmd/sync_test.go b/cmd/sync_test.go new file mode 100644 index 0000000..1eb5995 --- /dev/null +++ b/cmd/sync_test.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRemoveStaleExpansion_DeletesFile(t *testing.T) { + packDir := t.TempDir() + expandedPath := filepath.Join(packDir, "context.expanded.md") + require.NoError(t, os.WriteFile(expandedPath, []byte("stale"), 0644)) + + require.NoError(t, removeStaleExpansion(packDir)) + + _, err := os.Stat(expandedPath) + assert.True(t, os.IsNotExist(err), "context.expanded.md should be gone, got err=%v", err) +} + +func TestRemoveStaleExpansion_NoFileIsNotAnError(t *testing.T) { + packDir := t.TempDir() + // No context.expanded.md exists. + require.NoError(t, removeStaleExpansion(packDir)) +} + +func TestRemoveStaleExpansion_LeavesContextMdAlone(t *testing.T) { + packDir := t.TempDir() + contextPath := filepath.Join(packDir, "context.md") + expandedPath := filepath.Join(packDir, "context.expanded.md") + require.NoError(t, os.WriteFile(contextPath, []byte("source"), 0644)) + require.NoError(t, os.WriteFile(expandedPath, []byte("stale"), 0644)) + + require.NoError(t, removeStaleExpansion(packDir)) + + data, err := os.ReadFile(contextPath) + require.NoError(t, err) + assert.Equal(t, "source", string(data)) +} diff --git a/docs/content-authoring.md b/docs/content-authoring.md index 2a34f56..77ca8d9 100644 --- a/docs/content-authoring.md +++ b/docs/content-authoring.md @@ -263,6 +263,8 @@ For a plain-text or non-HTML source, use `format="raw"`: After `sap-devs sync`, the marker is expanded in `context.expanded.md` and the fetched release notes appear directly below it. The original `context.md` is never modified — only the derived `context.expanded.md` changes. +**Removing a marker.** If you delete every `` marker from a pack's `context.md`, the next `sap-devs sync` will delete the previously-written `context.expanded.md` for that pack. This ensures the loader falls back to the fresh `context.md` instead of continuing to serve stale fetched content. + For a real-world example see [`content/packs/cap/context.md`](../content/packs/cap/context.md). ---