feat(figma): drift detection and mapping cache reuse (Phase E — T2F.10)#43
feat(figma): drift detection and mapping cache reuse (Phase E — T2F.10)#43canonical-muhammadbassiony wants to merge 3 commits into
Conversation
T2F.10: LoadPreviousMeta looks up most recent matching run by docID + figmaFileKey T2F.10: LoadMappings reads mappings.json from a previous run artifact T2F.10: generateChunksWithFigma checks Figma version before re-fetching T2F.10: version unchanged => reuse stored mappings (skips GetNodes + screenshot DL) T2F.10: version changed => warn + full re-fetch T2F.10: RunMetadata.FigmaVersion stored after each figma fetch T2F.10: low-confidence/fallback mappings flagged with status: unresolved
There was a problem hiding this comment.
Pull request overview
Adds Figma drift detection so Bauer can skip expensive Figma fetches when the target file version hasn’t changed, and tightens mapping status normalization to avoid “silently promoted” low-confidence mappings.
Changes:
- Add
figma_versionto run metadata and index entries; introduce artifact-manager helpers for loading prior run metadata/mappings and updating the stored Figma version. - Implement drift detection in
generateChunksWithFigmawith a cache-hit early return that reuses stored mappings when the Figma version matches. - Normalize mapping outputs so low-confidence / fallback / none mappings are explicitly marked
Status: "unresolved".
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| internal/source/mapping/resolver.go | Adds a post-pass to enforce Status: "unresolved" for low-confidence/fallback/none mappings. |
| internal/orchestrator/orchestrator.go | Adds drift detection via GetMeta and a cache-hit early return that reuses prior mappings. |
| internal/artifacts/manager.go | Persists FigmaVersion and adds helpers to find prior run metadata, load mappings, and patch run metadata. |
| docs/implementation-log.md | Updates implementation log entry to describe Phase E / T2F.10 changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Version unchanged: reuse stored mappings and skip re-fetch. | ||
| slog.Info("Figma version unchanged; reusing stored mappings", | ||
| slog.String("version", currentMeta.Version), | ||
| slog.String("prev_run_id", prevRunMeta.RunID), | ||
| ) | ||
| resolvedChunks := o.arts.LoadMappings(prevRunMeta.RunID) | ||
| if resolvedChunks != nil { | ||
| return engine.RenderChunksFromResolved( | ||
| bundle.Document.DocumentTitle, | ||
| suggestedURL, | ||
| cfg.FigmaURL, | ||
| resolvedChunks, | ||
| cfg.ChunkSize, | ||
| cfg.OutputDir, | ||
| ) | ||
| } | ||
| slog.Warn("Previous mappings could not be loaded; proceeding with full re-fetch", | ||
| slog.String("prev_run_id", prevRunMeta.RunID)) |
There was a problem hiding this comment.
This is intentional. The drift check gates on Figma version only — it answers 'has the design changed?' If the Google Doc content changed, the user would simply re-run (the doc is always freshly fetched at the start of Execute). Coupling doc-change detection into the Figma drift path would conflate two independent concerns.
| return engine.RenderChunksFromResolved( | ||
| bundle.Document.DocumentTitle, | ||
| suggestedURL, | ||
| cfg.FigmaURL, | ||
| resolvedChunks, | ||
| cfg.ChunkSize, | ||
| cfg.OutputDir, | ||
| ) |
| // Drift detection: check whether the Figma file version has changed since last run. | ||
| currentMeta, err := figmaClient.GetMeta(ctx, figmaRef.FileKey) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("fetching figma metadata for drift check: %w", err) | ||
| } |
There was a problem hiding this comment.
Good catch — the meta fetched for drift detection could be threaded into FetchFigma to avoid the redundant call. This requires changing FetchFigma's signature and plumbing the response through, so tracking as a follow-up to keep this PR focused on drift detection correctness.
| // LoadPreviousMeta returns the RunMetadata from the most recent completed run | ||
| // that matches the given (docID, figmaFileKey) pair, or nil if none exists. | ||
| // It reads runs.jsonl in reverse order (last entry = most recent) to find the match. | ||
| func (m *Manager) LoadPreviousMeta(docID, figmaFileKey string) *RunMetadata { | ||
| indexPath := filepath.Join(m.base, "runs.jsonl") | ||
| f, err := os.Open(indexPath) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| defer f.Close() | ||
|
|
||
| // Collect matching entries (in file order; last = most recent). | ||
| var matched []RunIndexEntry | ||
| scanner := bufio.NewScanner(f) | ||
| for scanner.Scan() { |
| func (m *Manager) LoadPreviousMeta(docID, figmaFileKey string) *RunMetadata { | ||
| indexPath := filepath.Join(m.base, "runs.jsonl") | ||
| f, err := os.Open(indexPath) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| defer f.Close() | ||
|
|
There was a problem hiding this comment.
Agreed these need coverage. Will add unit tests for LoadPreviousMeta, LoadMappings, and UpdateRunFigmaVersion in a follow-up.
| // Enforce: low-confidence and fallback/none mappings must never be silently promoted. | ||
| for i := range chunks { | ||
| m := &chunks[i].Mapping | ||
| if m.Confidence < 0.5 || m.Method == "fallback" || m.Method == "none" { | ||
| m.Status = "unresolved" | ||
| } |
| **Summary:** Implemented drift detection and mapping cache reuse for Figma-backed runs. `RunMetadata` and `RunIndexEntry` gained a `FigmaVersion` field, and three new methods were added to `artifacts.Manager`: `LoadPreviousMeta` (scans `runs.jsonl` in reverse to find the most recent successful run with a matching DocID and Figma file key), `LoadMappings` (reads `extraction/mappings.json` from a prior run), and `UpdateRunFigmaVersion` (patches the current run's `metadata.json` after a fresh Figma fetch). In `generateChunksWithFigma`, a `GetMeta` call is now made before any other Figma API calls; if the version is unchanged versus the previous run, the stored mappings are reused and `GetNodes`/screenshot downloads are skipped; if changed, a warning is logged and a full re-fetch proceeds. `Resolver.Build` was hardened with a post-process normalization step that explicitly marks any chunk with `Confidence < 0.5`, `Method == "fallback"`, or `Method == "none"` as `Status: "unresolved"`, preventing silent promotion of low-quality mappings. | ||
|
|
||
| **Files changed:** _(to be filled by agent)_ | ||
| **Files changed:** | ||
| - `internal/artifacts/manager.go`: Added `FigmaVersion` field to `RunMetadata` and `RunIndexEntry`; added `LoadPreviousMeta`, `LoadMappings`, and `UpdateRunFigmaVersion` methods; added `bufio` import for JSONL scanning. | ||
| - `internal/orchestrator/orchestrator.go`: Rewrote `generateChunksWithFigma` to call `GetMeta` first for drift detection, consult `LoadPreviousMeta`/`LoadMappings` for cache reuse, log version changes as warnings, and call `UpdateRunFigmaVersion` after each fresh Figma fetch. |
| resolvedChunks := o.arts.LoadMappings(prevRunMeta.RunID) | ||
| if resolvedChunks != nil { | ||
| // Persist reused mappings into the current run so it is self-contained. | ||
| if runID != "" { | ||
| if err := o.arts.WriteMappings(runID, resolvedChunks); err != nil { |
| // Match figmaFileKey against the stored FigmaURL. | ||
| if entry.FigmaURL == "" { | ||
| continue | ||
| } | ||
| ref, err := figma.ParseLink(entry.FigmaURL) | ||
| if err != nil || ref.FileKey != figmaFileKey { |
There was a problem hiding this comment.
Acknowledged — this is a known limitation. LoadPreviousMeta currently matches by file key only, so running against a different node within the same file will incorrectly reuse stale mappings. Fixing this properly requires adding a node_id column to RunIndexEntry and updating the JSONL schema. Tracking as a follow-up for multi-node workflow support.
| continue | ||
| } | ||
| if entry.Status != "success" { | ||
| continue | ||
| } | ||
| if entry.DocID != docID { | ||
| continue | ||
| } |
| // Post-process: low-confidence, fallback, and unmatched ("none") mappings are | ||
| // promoted to status "unresolved" so they are never silently treated as healthy. | ||
| for i := range chunks { | ||
| m := &chunks[i].Mapping | ||
| if m.Confidence < 0.5 || m.Method == "fallback" || m.Method == "none" { | ||
| m.Status = "unresolved" | ||
| } |
| // LoadPreviousMeta returns the RunMetadata from the most recent completed run | ||
| // that matches the given (docID, figmaFileKey) pair, or nil if none exists. | ||
| // It scans runs.jsonl forward and returns metadata from the last matching entry. |
| continue | ||
| } | ||
| matched = append(matched, entry) | ||
| } |
| // Iterate backwards to find the most recent run with a non-empty FigmaVersion. | ||
| for i := len(matched) - 1; i >= 0; i-- { | ||
| metaPath := filepath.Join(m.base, matched[i].RunID, "metadata.json") | ||
| data, err := os.ReadFile(metaPath) | ||
| if err != nil { | ||
| continue |
Summary
Implements drift detection for Figma-backed runs. Bauer now skips expensive API calls when the design file has not changed since the last run.
Tasks Implemented
GetMetacalled first to retrieve file version before any other Figma API callsGetNodes/screenshot downloadsResolver.Buildhardened with post-process normalization: chunks withConfidence < 0.5,Method == "fallback", orMethod == "none"explicitly marked asStatus: "unresolved"Files Changed
internal/artifacts/manager.go—FigmaVersionfield;LoadPreviousMeta,LoadMappings,UpdateRunFigmaVersionmethodsinternal/orchestrator/orchestrator.go— drift detection ingenerateChunksWithFigma; cache-hit early-returninternal/source/mapping/resolver.go— post-process normalization loopPart of the Bauer v2 stacked PR series (Branch 8 of 12).