You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Decouple attribution interface from Automerge internals
Consumer-facing interfaces now expose CharAttribution[] entries
instead of the full AttributionMap (which carries Automerge-specific
processedHeads/processedHistoryIndex bookkeeping). This enables
swapping the attribution data source (e.g. to git blame) without
changing any consumer code.
Copy file name to clipboardExpand all lines: claude-notes/plans/2026-04-13-q2-debug-node-attribution.md
+22-5Lines changed: 22 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -63,6 +63,21 @@ The AST already carries source location info (`s` field on each node, referencin
63
63
-[x]`ReactAstDebugRenderer.tsx` — in `Node`, consume `NodeAttributionContext`, apply color + title tooltip
64
64
-[x] All Phase 3 tests pass (green) — 3 tests
65
65
66
+
### Phase 4: Decouple Attribution Interface from Automerge Internals
67
+
68
+
Refactor so that the consumer-facing interface carries only source-agnostic `CharAttribution[]` entries, not the full Automerge-specific `AttributionMap` (which includes `processedHeads` and `processedHistoryIndex` bookkeeping). This enables swapping the attribution data source (e.g., to git blame) without changing any consumer code.
69
+
70
+
-[x]`getNodeAttribution()` takes `entries: CharAttribution[]` instead of `attributionMap: AttributionMap`
71
+
-[x]`AttributionContext` shape: `entries: CharAttribution[]` instead of `attributionMap: AttributionMap`
72
+
-[x]`UseAttributionResult` type: `entries: CharAttribution[]` instead of `attributionMap: AttributionMap`
73
+
-[x]`useAttribution` hook internally retains `AttributionMap` for incremental updates but only surfaces `.entries` to consumers
-[x] Tests updated: service tests pass `CharAttribution[]` directly; hook tests assert on `.entries`
77
+
-[x] All 453 tests pass, TypeScript type-checks cleanly
78
+
79
+
**Rationale**: The `processedHeads` and `processedHistoryIndex` fields are Automerge incremental-update bookkeeping — they only matter inside the hook, never to consumers. With this refactor, a future git blame provider only needs to produce `{ entries: CharAttribution[], byteToCharMap: number[] }` (the same hook return shape), and everything downstream works unchanged.
80
+
66
81
### Verification
67
82
68
83
-[x]`npm run build:all` passes from hub-client (after annotated-qmd fixes in `c6200953`)
@@ -254,7 +269,7 @@ export function updateAttributionMap(
**IMPORTANT — `content` is NOT in the WASM output**: The Rust JSON writer (`crates/pampa/src/writers/json.rs:66-72`) serializes `FileEntryJson` with only `{ line_breaks?, name, total_length? }` — no `content` field. The `SourceInfoReconstructor` constructor **throws** if `file.content` is null/undefined (`ts-packages/annotated-qmd/src/source-map.ts:63-67`). Before constructing the reconstructor, the QMD source text must be injected into `astContext.files[0].content`. This text is the same Automerge document content that was passed to the WASM parser. The `Ast` component must receive this text (via `AttributionContext` or as a prop) to populate the field.
286
301
287
-
**Query function**: `getNodeAttribution(sourceInfoId, reconstructor, attributionMap, byteToCharMap, identities) -> NodeAttribution | null` — calls `reconstructor.getSourceLocation(sourceInfoId)` to get `{ fileId, start, end }` in top-level byte coordinates, converts to char range via `byteToCharMap`, finds the most recent `{ actor, time }` in that range, resolves identity.
302
+
**Query function**: `getNodeAttribution(sourceInfoId, reconstructor, entries, byteToCharMap, identities) -> NodeAttribution | null` — calls `reconstructor.getSourceLocation(sourceInfoId)` to get `{ fileId, start, end }` in top-level byte coordinates, converts to char range via `byteToCharMap`, finds the most recent `{ actor, time }` in the `CharAttribution[]` entries for that range, resolves identity.
3.**Cancellation on file switch**: When `filePath` changes, the hook aborts the in-flight build (if any) via `abortController.abort()`, resets state to `null`, and starts a fresh build for the new file.
297
312
4.**Cancellation on unmount**: Cleanup function aborts any in-flight build.
298
313
5.**Fallback to full rebuild**: If `updateAttributionMap` detects history compaction (stored index exceeds history length) or `diff()` fails, it throws a `HistoryCompactedError`. The hook catches this and starts a new async `buildAttributionMap` (returning `null` in the interim until it completes).
299
-
6. Returns `{ attributionMap, byteToCharMap }` or `null`
314
+
6. Returns `{ entries, byteToCharMap }` or `null` (entries is `CharAttribution[]`, extracted from the internal `AttributionMap`)
300
315
301
316
**Ref stability**: The hook stores the `AttributionMap` in a `useRef` alongside the React state setter, so the debounced callback always operates on the latest map without needing the state value in its closure (avoids stale-closure bugs with rapid edits).
302
317
303
318
**Context definition** (in the same file or a shared types file):
sourceText:string; // QMD text from Automerge doc, needed to populate astContext.files[].content
310
325
} |null>(null);
311
326
```
312
327
328
+
**Design note (Phase 4 refactor)**: The context deliberately exposes `entries: CharAttribution[]` instead of the full `AttributionMap`. The Automerge-specific bookkeeping (`processedHeads`, `processedHistoryIndex`) stays internal to the hook. This allows a future git blame provider to produce the same context shape without any consumer changes.
329
+
313
330
### Phase 3: Wire Context in Editor, Extract + Render in Ast
314
331
315
332
**Attribution data flows via context**, avoiding prop drilling through PreviewRouter, ReactPreview, and ReactRenderer:
316
333
317
-
-[ ]`Editor.tsx` — call `useAttribution(currentFile?.path, identities)`, wrap preview area with `AttributionContext.Provider` passing `{ attributionMap, byteToCharMap, identities, sourceText }` where `sourceText` is the current Automerge document content (already available as the text passed to the preview pipeline)
334
+
-[ ]`Editor.tsx` — call `useAttribution(currentFile?.path, identities)`, wrap preview area with `AttributionContext.Provider` passing `{ entries, byteToCharMap, identities, sourceText }` where `sourceText` is the current Automerge document content (already available as the text passed to the preview pipeline)
318
335
319
336
The `astJson` string flowing through `ReactPreview → ReactRenderer → Ast` is already the full `RustQmdJson` — it contains `astContext` and per-node `s` fields at runtime. No upstream component needs to extract or thread this data.
0 commit comments