Files
weichenandCursor 268a9bdb56 feat(service): implement docrevision Version/Revision kernel
Add canonicalize, stable block map, diff, risk floor, memory ledger
service, contract fixtures, concurrency tests, and backfill CLI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 06:15:20 +00:00

186 lines
4.4 KiB
Go

package docrevision
import (
"crypto/rand"
"encoding/hex"
"fmt"
"regexp"
"strings"
)
var (
headingRe = regexp.MustCompile(`^(#{1,6})\s+(.+)$`)
reqIDRe = regexp.MustCompile(`\b(REQ-[A-Z0-9-]+)\b`)
listItemRe = regexp.MustCompile(`^(\s*[-*+]|\s*\d+\.)\s+`)
tableRowRe = regexp.MustCompile(`^\|`)
fenceRe = regexp.MustCompile("^```")
mermaidRe = regexp.MustCompile(`(?i)^mermaid\b`)
)
// BuildBlockMap parses markdown into stable block nodes.
// When previous is provided, nodes are aligned by business key → heading path → fingerprint → order.
func BuildBlockMap(markdown string, previous []BlockNode) []BlockNode {
lines := strings.Split(strings.TrimSuffix(markdown, "\n"), "\n")
var nodes []BlockNode
offset := 0
headingPath := []string{}
inFence := false
fenceLang := ""
var fenceBuf []string
fenceStart := 0
order := 0
flushFence := func() {
if !inFence {
return
}
body := strings.Join(fenceBuf, "\n")
typ := "code"
if mermaidRe.MatchString(fenceLang) {
typ = "mermaid"
}
nodes = append(nodes, makeNode(typ, fenceStart, offset, strings.Join(headingPath, " / "), "", body, order, previous))
order++
inFence = false
fenceBuf = nil
fenceLang = ""
}
for i, line := range lines {
lineLen := len(line)
if i < len(lines)-1 || strings.HasSuffix(markdown, "\n") {
lineLen++ // account for newline except possibly last without NL; we trimSuffix above so always +1 except last
}
// recompute offset carefully
_ = lineLen
}
offset = 0
for i, line := range lines {
start := offset
end := start + len(line)
if i < len(lines)-1 {
end++ // newline
} else if strings.HasSuffix(markdown, "\n") {
end++
}
if fenceRe.MatchString(line) {
if !inFence {
inFence = true
fenceLang = strings.TrimSpace(strings.TrimPrefix(line, "```"))
fenceStart = start
fenceBuf = nil
} else {
flushFence()
}
offset = end
continue
}
if inFence {
fenceBuf = append(fenceBuf, line)
offset = end
continue
}
if m := headingRe.FindStringSubmatch(line); m != nil {
level := len(m[1])
title := strings.TrimSpace(m[2])
if level <= len(headingPath) {
headingPath = headingPath[:level-1]
}
headingPath = append(headingPath, title)
path := strings.Join(headingPath, " / ")
bk := ""
if id := reqIDRe.FindString(title); id != "" {
bk = id
}
nodes = append(nodes, makeNode("heading", start, end, path, bk, line, order, previous))
order++
offset = end
continue
}
path := strings.Join(headingPath, " / ")
switch {
case tableRowRe.MatchString(line):
bk := ""
if id := reqIDRe.FindString(line); id != "" {
bk = id
}
nodes = append(nodes, makeNode("table_row", start, end, path, bk, line, order, previous))
case listItemRe.MatchString(line):
bk := ""
if id := reqIDRe.FindString(line); id != "" {
bk = id
}
nodes = append(nodes, makeNode("list_item", start, end, path, bk, line, order, previous))
case strings.TrimSpace(line) == "":
// skip empty
default:
bk := ""
if id := reqIDRe.FindString(line); id != "" {
bk = id
}
nodes = append(nodes, makeNode("paragraph", start, end, path, bk, line, order, previous))
}
order++
offset = end
}
flushFence()
return nodes
}
func makeNode(typ string, start, end int, path, businessKey, text string, order int, previous []BlockNode) BlockNode {
fp := ContentFingerprint(text)
id := matchPreviousID(typ, path, businessKey, fp, order, previous)
if id == "" {
id = newBlockID()
}
return BlockNode{
ID: id,
Type: typ,
StartOffset: start,
EndOffset: end,
HeadingPath: path,
BusinessKey: businessKey,
Fingerprint: fp,
Order: order,
}
}
func matchPreviousID(typ, path, businessKey, fp string, order int, previous []BlockNode) string {
if len(previous) == 0 {
return ""
}
if businessKey != "" {
for _, p := range previous {
if p.BusinessKey == businessKey && p.Type == typ {
return p.ID
}
}
}
for _, p := range previous {
if p.Type == typ && p.HeadingPath == path && p.Fingerprint == fp {
return p.ID
}
}
for _, p := range previous {
if p.Type == typ && p.Fingerprint == fp {
return p.ID
}
}
for _, p := range previous {
if p.Order == order && p.Type == typ {
return p.ID
}
}
return ""
}
func newBlockID() string {
var b [8]byte
_, _ = rand.Read(b[:])
return fmt.Sprintf("blk_%s", hex.EncodeToString(b[:]))
}