Files
nexus/internal/leviathan/analysis/diff.go
T
Andy DixonandClaude Sonnet 5 1a380a0e99 Rebrand: Nexus Terminal → Alchementary Link
Renames the product across the codebase: Go module path, config/data
paths, keychain service name, export file extension, build tooling,
docs, and UI text. Sidebar brand mark now reads ">_ AlchementaryLink"
with "Alchementary" in the connection-group gold accent colour, and
the window title is "Alchementary Link".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T8bJS8BCtY5GeSG2HBj1n
2026-09-03 16:41:23 +01:00

245 lines
5.3 KiB
Go

package analysis
import (
"context"
"github.com/andydixon/link/internal/leviathan/document"
)
// DiffRegion describes a contiguous run of differing bytes.
type DiffRegion struct {
Offset int64 `json:"offset"`
Length int64 `json:"length"`
// kinds: "edit" (both sides have bytes), "add" (only B), "remove" (only A)
Kind string `json:"kind"`
}
type DiffBlock struct {
Offset int64 `json:"offset"`
Length int64 `json:"length"`
ChangedBytes int64 `json:"changedBytes"`
EditBytes int64 `json:"editBytes"`
AddBytes int64 `json:"addBytes"`
RemoveBytes int64 `json:"removeBytes"`
Similarity float64 `json:"similarity"`
}
// Diff compares two documents by byte. It handles documents of
// different sizes by treating the tail of the longer as add/remove.
// Chunks at both sides. Returns a compact list of regions.
func Diff(a, b *document.Document, start, length int64) (map[string]any, error) {
return DiffContext(context.Background(), a, b, start, length)
}
func DiffContext(ctx context.Context, a, b *document.Document, start, length int64) (map[string]any, error) {
aSize := a.Size()
bSize := b.Size()
maxSize := aSize
if bSize > maxSize {
maxSize = bSize
}
if length < 0 {
length = maxSize - start
}
end := start + length
if end > maxSize {
end = maxSize
}
const chunk = 1 << 20
regions := make([]DiffRegion, 0, 64)
var inRun bool
var runStart int64
var runKind string
flush := func(off int64) {
if inRun {
regions = append(regions, DiffRegion{Offset: runStart, Length: off - runStart, Kind: runKind})
inRun = false
}
}
for off := start; off < end; {
if err := ctx.Err(); err != nil {
return nil, err
}
winEnd := off + chunk
if winEnd > end {
winEnd = end
}
// Read both sides, tolerating short reads for the shorter file.
var ab, bb []byte
if off < aSize {
n := winEnd - off
if off+n > aSize {
n = aSize - off
}
var err error
ab, err = a.Range(off, n)
if err != nil {
return nil, err
}
}
if off < bSize {
n := winEnd - off
if off+n > bSize {
n = bSize - off
}
var err error
bb, err = b.Range(off, n)
if err != nil {
return nil, err
}
}
for i := int64(0); i < winEnd-off; i++ {
if i%4096 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
curOff := off + i
var av, bv byte
var av_ok, bv_ok bool
if curOff < aSize && int64(len(ab)) > i {
av = ab[i]
av_ok = true
}
if curOff < bSize && int64(len(bb)) > i {
bv = bb[i]
bv_ok = true
}
kind := ""
if av_ok && bv_ok {
if av != bv {
kind = "edit"
}
} else if av_ok && !bv_ok {
kind = "remove"
} else if !av_ok && bv_ok {
kind = "add"
}
if kind == "" {
flush(curOff)
continue
}
if !inRun {
inRun = true
runStart = curOff
runKind = kind
} else if kind != runKind {
flush(curOff)
inRun = true
runStart = curOff
runKind = kind
}
}
off = winEnd
}
flush(end)
// Convert to JS-friendly output + summary
out := make([]map[string]any, len(regions))
editBytes := int64(0)
addBytes := int64(0)
removeBytes := int64(0)
for i, r := range regions {
out[i] = map[string]any{
"offset": r.Offset,
"length": r.Length,
"kind": r.Kind,
}
switch r.Kind {
case "edit":
editBytes += r.Length
case "add":
addBytes += r.Length
case "remove":
removeBytes += r.Length
}
}
const blockSize int64 = 4096
blocks := diffBlocksFromRegions(regions, start, end, blockSize)
blockOut := make([]map[string]any, len(blocks))
for i, b := range blocks {
blockOut[i] = map[string]any{
"offset": b.Offset,
"length": b.Length,
"changedBytes": b.ChangedBytes,
"editBytes": b.EditBytes,
"addBytes": b.AddBytes,
"removeBytes": b.RemoveBytes,
"similarity": b.Similarity,
}
}
return map[string]any{
"regions": out,
"regionCount": len(regions),
"blocks": blockOut,
"blockCount": len(blocks),
"blockSize": blockSize,
"editBytes": editBytes,
"addBytes": addBytes,
"removeBytes": removeBytes,
"aSize": aSize,
"bSize": bSize,
}, nil
}
func diffBlocksFromRegions(regions []DiffRegion, start, end, blockSize int64) []DiffBlock {
if blockSize <= 0 || end <= start || len(regions) == 0 {
return nil
}
out := []DiffBlock{}
for alignedStart := start - start%blockSize; alignedStart < end; alignedStart += blockSize {
blockStart := alignedStart
blockEnd := alignedStart + blockSize
if blockEnd > end {
blockEnd = end
}
if blockEnd <= start {
continue
}
if blockStart < start {
blockStart = start
}
b := DiffBlock{Offset: blockStart, Length: blockEnd - blockStart}
for _, r := range regions {
rEnd := r.Offset + r.Length
if rEnd <= blockStart {
continue
}
if r.Offset >= blockEnd {
break
}
ovStart := r.Offset
if ovStart < blockStart {
ovStart = blockStart
}
ovEnd := rEnd
if ovEnd > blockEnd {
ovEnd = blockEnd
}
changed := ovEnd - ovStart
if changed <= 0 {
continue
}
b.ChangedBytes += changed
switch r.Kind {
case "edit":
b.EditBytes += changed
case "add":
b.AddBytes += changed
case "remove":
b.RemoveBytes += changed
}
}
if b.ChangedBytes == 0 {
continue
}
b.Similarity = 1 - float64(b.ChangedBytes)/float64(b.Length)
out = append(out, b)
}
return out
}