Knowledge base of ~80+ markdown files across 14 domains (00-13), Logseq graph, hardware design files (KiCAD), infrastructure configs, and talas-wiki static site. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
190 lines
4.2 KiB
Go
190 lines
4.2 KiB
Go
package wiki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Comment struct {
|
|
ID string `json:"id"`
|
|
Author string `json:"author"`
|
|
Content string `json:"content"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type PageComments struct {
|
|
PagePath string `json:"page_path"`
|
|
Comments []Comment `json:"comments"`
|
|
}
|
|
|
|
type CommentStore struct {
|
|
mu sync.RWMutex
|
|
dataDir string
|
|
}
|
|
|
|
func NewCommentStore(docsRoot string) *CommentStore {
|
|
dir := filepath.Join(docsRoot, ".wiki-data", "comments")
|
|
os.MkdirAll(dir, 0755)
|
|
return &CommentStore{dataDir: dir}
|
|
}
|
|
|
|
func (cs *CommentStore) filePath(pagePath string) string {
|
|
safe := filepath.Clean(pagePath)
|
|
safe = filepath.Join(cs.dataDir, safe+".json")
|
|
return safe
|
|
}
|
|
|
|
func (cs *CommentStore) Get(pagePath string) []Comment {
|
|
cs.mu.RLock()
|
|
defer cs.mu.RUnlock()
|
|
|
|
data, err := os.ReadFile(cs.filePath(pagePath))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var comments []Comment
|
|
json.Unmarshal(data, &comments)
|
|
return comments
|
|
}
|
|
|
|
func (cs *CommentStore) Add(pagePath, author, content string) Comment {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
|
|
comments := cs.readUnsafe(pagePath)
|
|
c := Comment{
|
|
ID: time.Now().Format("20060102150405"),
|
|
Author: author,
|
|
Content: content,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
comments = append(comments, c)
|
|
cs.writeUnsafe(pagePath, comments)
|
|
return c
|
|
}
|
|
|
|
func (cs *CommentStore) Delete(pagePath, commentID string) {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
|
|
comments := cs.readUnsafe(pagePath)
|
|
var filtered []Comment
|
|
for _, c := range comments {
|
|
if c.ID != commentID {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
cs.writeUnsafe(pagePath, filtered)
|
|
}
|
|
|
|
func (cs *CommentStore) readUnsafe(pagePath string) []Comment {
|
|
data, err := os.ReadFile(cs.filePath(pagePath))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var comments []Comment
|
|
json.Unmarshal(data, &comments)
|
|
return comments
|
|
}
|
|
|
|
func (cs *CommentStore) writeUnsafe(pagePath string, comments []Comment) {
|
|
sort.Slice(comments, func(i, j int) bool {
|
|
return comments[i].CreatedAt.Before(comments[j].CreatedAt)
|
|
})
|
|
data, _ := json.MarshalIndent(comments, "", " ")
|
|
fp := cs.filePath(pagePath)
|
|
os.MkdirAll(filepath.Dir(fp), 0755)
|
|
os.WriteFile(fp, data, 0644)
|
|
}
|
|
|
|
// Review proposals: stored as JSON with proposed content
|
|
|
|
type ReviewProposal struct {
|
|
ID string `json:"id"`
|
|
PagePath string `json:"page_path"`
|
|
Author string `json:"author"`
|
|
Message string `json:"message"`
|
|
NewContent string `json:"new_content"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type ReviewStore struct {
|
|
mu sync.RWMutex
|
|
dataDir string
|
|
}
|
|
|
|
func NewReviewStore(docsRoot string) *ReviewStore {
|
|
dir := filepath.Join(docsRoot, ".wiki-data", "reviews")
|
|
os.MkdirAll(dir, 0755)
|
|
return &ReviewStore{dataDir: dir}
|
|
}
|
|
|
|
func (rs *ReviewStore) List() []ReviewProposal {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
|
|
entries, err := os.ReadDir(rs.dataDir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var proposals []ReviewProposal
|
|
for _, e := range entries {
|
|
if filepath.Ext(e.Name()) != ".json" {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(rs.dataDir, e.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var p ReviewProposal
|
|
if json.Unmarshal(data, &p) == nil {
|
|
proposals = append(proposals, p)
|
|
}
|
|
}
|
|
sort.Slice(proposals, func(i, j int) bool {
|
|
return proposals[i].CreatedAt.After(proposals[j].CreatedAt)
|
|
})
|
|
return proposals
|
|
}
|
|
|
|
func (rs *ReviewStore) Get(id string) *ReviewProposal {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
|
|
data, err := os.ReadFile(filepath.Join(rs.dataDir, id+".json"))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var p ReviewProposal
|
|
if json.Unmarshal(data, &p) != nil {
|
|
return nil
|
|
}
|
|
return &p
|
|
}
|
|
|
|
func (rs *ReviewStore) Create(pagePath, author, message, content string) ReviewProposal {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
|
|
p := ReviewProposal{
|
|
ID: time.Now().Format("20060102150405"),
|
|
PagePath: pagePath,
|
|
Author: author,
|
|
Message: message,
|
|
NewContent: content,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
data, _ := json.MarshalIndent(p, "", " ")
|
|
os.WriteFile(filepath.Join(rs.dataDir, p.ID+".json"), data, 0644)
|
|
return p
|
|
}
|
|
|
|
func (rs *ReviewStore) Delete(id string) {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
os.Remove(filepath.Join(rs.dataDir, id+".json"))
|
|
}
|