acf62ebc
feat(rewriter): atomic UUID stamping with mode preservation
a73x 2026-04-29 05:49
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff --git a/internal/rewriter/rewriter.go b/internal/rewriter/rewriter.go new file mode 100644 index 0000000..2fab15b --- /dev/null +++ b/internal/rewriter/rewriter.go @@ -0,0 +1,116 @@ package rewriter import ( "bufio" "errors" "fmt" "os" "path/filepath" "regexp" "strings" ) var ErrConcurrentModification = errors.New("file modified concurrently") // stampRE matches "@claude" optionally followed by an existing "[fw-XXXXXXXX]" tag. var stampRE = regexp.MustCompile(`@claude(?:\[fw-[0-9a-f]{8}\])?`) // StampUUIDs rewrites the file at path, splicing "[fw-XXXXXXXX]" immediately // after the first untagged "@claude" on each given line. stamps maps line // number (1-indexed) → UUID (e.g. "fw-a1b2c3d4"). The write is atomic via // tmpfile + rename. File mode is preserved. // // Aborts with ErrConcurrentModification if the file's mtime changed between // read and rename (someone — likely the user's editor — wrote concurrently). // In that case, the rewrite is dropped and the marker stays untagged for a // retry on the next save event. func StampUUIDs(path string, stamps map[int]string) error { info, err := os.Stat(path) if err != nil { return err } mtimeBefore := info.ModTime() in, err := os.Open(path) if err != nil { return err } defer in.Close() dir := filepath.Dir(path) tmp, err := os.CreateTemp(dir, ".filewatch-*.tmp") if err != nil { return err } tmpPath := tmp.Name() cleanup := func() { tmp.Close() os.Remove(tmpPath) } sc := bufio.NewScanner(in) sc.Buffer(make([]byte, 1024*1024), 1024*1024) bw := bufio.NewWriter(tmp) line := 0 for sc.Scan() { line++ text := sc.Text() if uuid, ok := stamps[line]; ok { text = stampLine(text, uuid) } if _, err := bw.WriteString(text); err != nil { cleanup() return err } if _, err := bw.WriteString("\n"); err != nil { cleanup() return err } } if err := sc.Err(); err != nil { cleanup() return err } if err := bw.Flush(); err != nil { cleanup() return err } if err := tmp.Chmod(info.Mode().Perm()); err != nil { cleanup() return err } if err := tmp.Close(); err != nil { os.Remove(tmpPath) return err } // mtime check before atomic rename (deterministic test for this comes in Task 7) info2, err := os.Stat(path) if err != nil { os.Remove(tmpPath) return err } if !info2.ModTime().Equal(mtimeBefore) { os.Remove(tmpPath) return ErrConcurrentModification } if err := os.Rename(tmpPath, path); err != nil { os.Remove(tmpPath) return fmt.Errorf("rename: %w", err) } return nil } // stampLine splices "[uuid]" after the first untagged "@claude" on the line. // Already-tagged occurrences are left untouched. func stampLine(text, uuid string) string { done := false return stampRE.ReplaceAllStringFunc(text, func(m string) string { if done || strings.Contains(m, "[fw-") { return m } done = true return "@claude[" + uuid + "]" }) } diff --git a/internal/rewriter/rewriter_test.go b/internal/rewriter/rewriter_test.go new file mode 100644 index 0000000..d60835d --- /dev/null +++ b/internal/rewriter/rewriter_test.go @@ -0,0 +1,65 @@ package rewriter import ( "os" "path/filepath" "strings" "testing" ) func TestStampUUIDsInsertsTagAfterClaude(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "foo.go") original := "package foo\n\n// @claude write a test\nfunc Foo() {}\n" if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatal(err) } err := StampUUIDs(path, map[int]string{3: "fw-a1b2c3d4"}) if err != nil { t.Fatalf("StampUUIDs: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } want := "package foo\n\n// @claude[fw-a1b2c3d4] write a test\nfunc Foo() {}\n" if string(got) != want { t.Errorf("got:\n%s\nwant:\n%s", got, want) } } func TestStampUUIDsPreservesIndentation(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "foo.go") original := "func F() {\n // @claude refactor\n}\n" if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatal(err) } if err := StampUUIDs(path, map[int]string{2: "fw-deadbeef"}); err != nil { t.Fatal(err) } got, _ := os.ReadFile(path) if !strings.Contains(string(got), " // @claude[fw-deadbeef] refactor") { t.Errorf("indentation not preserved:\n%s", got) } } func TestStampUUIDsBlockComment(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "x.html") original := "<!-- @claude add aria -->\n<div></div>\n" if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatal(err) } if err := StampUUIDs(path, map[int]string{1: "fw-12345678"}); err != nil { t.Fatal(err) } got, _ := os.ReadFile(path) want := "<!-- @claude[fw-12345678] add aria -->\n<div></div>\n" if string(got) != want { t.Errorf("got:\n%s\nwant:\n%s", got, want) } }