feat: Implement foundation layer with domain entities and repository interfaces

- Add complete domain layer: Note, Vault, WikiLink, Tag, Frontmatter, Graph entities
- Implement repository interfaces for data access abstraction
- Create comprehensive configuration system with YAML and env support
- Add CLI entry point with signal handling and graceful shutdown
- Fix mermaid diagram syntax in design.md (array notation)
- Add CLAUDE.md for development guidance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev
2025-10-08 10:22:28 +04:00
parent b655c58ba1
commit da289d4a7e
24 changed files with 1840 additions and 7 deletions

190
internal/domain/note.go Normal file
View File

@@ -0,0 +1,190 @@
package domain
import (
"path/filepath"
"regexp"
"strings"
"time"
)
type Note struct {
path string
content string
frontmatter *Frontmatter
outlinks []*WikiLink
tags []*Tag
createdAt time.Time
modifiedAt time.Time
}
func NewNote(path, content string) (*Note, error) {
if err := validatePath(path); err != nil {
return nil, err
}
note := &Note{
path: path,
content: content,
frontmatter: NewFrontmatter(),
outlinks: make([]*WikiLink, 0),
tags: make([]*Tag, 0),
createdAt: time.Now(),
modifiedAt: time.Now(),
}
if err := note.extractLinks(); err != nil {
return nil, err
}
if err := note.extractTags(); err != nil {
return nil, err
}
return note, nil
}
func (n *Note) Path() string {
return n.path
}
func (n *Note) Content() string {
return n.content
}
func (n *Note) Frontmatter() *Frontmatter {
return n.frontmatter
}
func (n *Note) Outlinks() []*WikiLink {
return n.outlinks
}
func (n *Note) Tags() []*Tag {
return n.tags
}
func (n *Note) CreatedAt() time.Time {
return n.createdAt
}
func (n *Note) ModifiedAt() time.Time {
return n.modifiedAt
}
func (n *Note) UpdateContent(content string) error {
n.content = content
n.modifiedAt = time.Now()
n.outlinks = make([]*WikiLink, 0)
n.tags = make([]*Tag, 0)
if err := n.extractLinks(); err != nil {
return err
}
return n.extractTags()
}
func (n *Note) SetFrontmatter(fm *Frontmatter) {
n.frontmatter = fm
n.modifiedAt = time.Now()
}
func (n *Note) AddTag(tag *Tag) error {
for _, existingTag := range n.tags {
if existingTag.Name() == tag.Name() {
return nil
}
}
n.tags = append(n.tags, tag)
n.modifiedAt = time.Now()
return nil
}
func (n *Note) RemoveTag(tagName string) error {
for i, tag := range n.tags {
if tag.Name() == tagName {
n.tags = append(n.tags[:i], n.tags[i+1:]...)
n.modifiedAt = time.Now()
return nil
}
}
return nil
}
func (n *Note) HasTag(tagName string) bool {
for _, tag := range n.tags {
if tag.Name() == tagName {
return true
}
}
return false
}
func (n *Note) ExtractLinks() []*WikiLink {
return n.outlinks
}
func (n *Note) WordCount() int {
words := strings.Fields(n.content)
return len(words)
}
func (n *Note) CharacterCount() int {
return len(n.content)
}
func (n *Note) extractLinks() error {
linkRegex := regexp.MustCompile(`\[\[([^\]]+)\]\]`)
matches := linkRegex.FindAllStringSubmatch(n.content, -1)
for _, match := range matches {
if len(match) >= 2 {
link, err := ParseWikiLink(match[1])
if err != nil {
continue
}
n.outlinks = append(n.outlinks, link)
}
}
return nil
}
func (n *Note) extractTags() error {
tagRegex := regexp.MustCompile(`#([a-zA-Z0-9_/]+)`)
matches := tagRegex.FindAllStringSubmatch(n.content, -1)
for _, match := range matches {
if len(match) >= 2 {
tag, err := NewTag(match[1])
if err != nil {
continue
}
n.AddTag(tag)
}
}
return nil
}
func validatePath(path string) error {
if path == "" {
return ErrInvalidPath
}
if !strings.HasSuffix(path, ".md") {
return ErrInvalidPath
}
if strings.Contains(path, "..") {
return ErrInvalidPath
}
if !filepath.IsAbs(path) && strings.HasPrefix(path, "/") {
return ErrInvalidPath
}
return nil
}